@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
package/dist/index.js ADDED
@@ -0,0 +1,85 @@
1
+ export { cn } from "./cn";
2
+ export { tokens, NativeUiProvider, useNativeUi, useLargerText, useAccentColor, DEFAULT_ACCENT } from "./theme";
3
+ export { palette, withAlpha } from "./palette";
4
+ export { configureTheme } from "./configureTheme";
5
+ export { fontClass, fontFamily } from "./typography";
6
+ export { FLOATING_INSET, FLOATING_TAB_BAR_HEIGHT, FLOATING_TAB_BAR_HEIGHT_LARGE, FLOATING_RADIUS, FLOATING_ROW_RADIUS, FLOATING_PILL_RADIUS, floatingShadow, floatingHairline, floatingFill, floatingSolidFill, tabBarReserve } from "./floating";
7
+ export { impactHaptic, notificationHaptic, selectionHaptic } from "./haptics";
8
+ export { useReduceTransparency } from "./useReduceTransparency";
9
+ export { FloatingSurface, GlassSheetBackground } from "./FloatingSurface";
10
+ export { LiquidGlass, GlassBlurProvider, GlassBlurTarget } from "./LiquidGlass";
11
+ export { AmbientBackground } from "./AmbientBackground";
12
+ export { FloatingTabBar } from "./FloatingTabBar";
13
+ export { FloatingFAB } from "./FloatingFAB";
14
+ export { Button } from "./Button";
15
+ export { Input } from "./Input";
16
+ export { SearchInput } from "./SearchInput";
17
+ export { PasswordInput } from "./PasswordInput";
18
+ export { Textarea } from "./Textarea";
19
+ export { Label } from "./Label";
20
+ export { Select } from "./Select";
21
+ export { Switch } from "./Switch";
22
+ export { LabeledSwitchRow } from "./LabeledSwitchRow";
23
+ export { Checkbox } from "./Checkbox";
24
+ export { Badge } from "./Badge";
25
+ export { StatusDot } from "./StatusDot";
26
+ export { ColorSwatches } from "./ColorSwatches";
27
+ export { StatGrid } from "./StatGrid";
28
+ export { SectionLabel } from "./SectionLabel";
29
+ export { SheetRow } from "./SheetRow";
30
+ export { PageIndicator } from "./PageIndicator";
31
+ export { IconButton } from "./IconButton";
32
+ export { CaptureButton } from "./CaptureButton";
33
+ export { Separator } from "./Separator";
34
+ export { Progress } from "./Progress";
35
+ export { Spinner, CenteredSpinner } from "./Spinner";
36
+ export { KeyboardAvoidingView } from "./primitives/keyboard-parts";
37
+ export { LoadingState } from "./LoadingState";
38
+ export { Avatar, AvatarImage, AvatarFallback } from "./Avatar";
39
+ export { initials } from "./initials";
40
+ export { formatCompactRelativeTime } from "./relativeTime";
41
+ export { Chip } from "./Chip";
42
+ export { PillTabs } from "./PillTabs";
43
+ export { Pager } from "./Pager";
44
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from "./Tabs";
45
+ export { ListRow } from "./ListRow";
46
+ export { DividedList, ListDivider } from "./DividedList";
47
+ export { SwipeableRow } from "./SwipeableRow";
48
+ export { closeSwipeRows, swipeActionsAccessibility } from "./swipeRow";
49
+ export { ImageGallery, ImageViewer } from "./media/ImageGallery";
50
+ export { MediaCard } from "./media/MediaCard";
51
+ export { imageAspect, imageGridTiles, imageViewerPage, MEDIA } from "./media/imageGrid";
52
+ export { SurfaceRow, SURFACE_ROW } from "./SurfaceRow";
53
+ export { HeaderIconButton } from "./HeaderIconButton";
54
+ export { ThemeToggle } from "./ThemeToggle";
55
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "./Card";
56
+ export { Skeleton, CardSkeleton, SkeletonList, SkeletonRow } from "./Skeleton";
57
+ export { EmptyState } from "./EmptyState";
58
+ export { Dialog, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter } from "./Dialog";
59
+ export { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogAction, AlertDialogCancel } from "./AlertDialog";
60
+ export { ConfirmDialog } from "./ConfirmDialog";
61
+ export { BulkActionBar, BULK_BAR_LABEL_MIN_WIDTH } from "./BulkActionBar";
62
+ export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuItem, DropdownMenuRadioItem, DropdownMenuSeparator } from "./DropdownMenu";
63
+ export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHandle, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter } from "./Sheet";
64
+ export { Toaster } from "./Toast";
65
+ export { toast } from "./toastStore";
66
+ export { DateTimeField } from "./DateTimeField";
67
+ export { BottomSheetContainer } from "./BottomSheetContainer";
68
+ export { SafeAreaContainer } from "./SafeAreaContainer";
69
+ export { ScreenContainer } from "./ScreenContainer";
70
+ export { PageTitle, PageHeader } from "./PageTitle";
71
+ export { StackPageHeader } from "./StackPageHeader";
72
+ export { SettingsSection } from "./settings/SettingsSection";
73
+ export { SettingsField } from "./settings/SettingsField";
74
+ export { SettingsToggleRow } from "./settings/SettingsToggleRow";
75
+ export { SettingsStack } from "./settings/SettingsStack";
76
+ export { SettingsEmptyState } from "./settings/SettingsEmptyState";
77
+ export { PinnedBotTile, BotPinGrid } from "./PinnedBotTile";
78
+ export { ArtifactList } from "./ArtifactList";
79
+ export { MoreFromProjectV, FromProjectV } from "./settings/MoreFromProjectV";
80
+ export { PROJECT_V_MOBILE_APPS, projectVMobileApp, otherProjectVMobileApps, openProjectVApp, projectVAppOpenSteps, VSTORE_HOME_URL, androidIntentUrl } from "./settings/projectVMobileApps";
81
+ export { PINK_50, PINK_100, PINK_600, PINK_700, NEUTRAL_100, NEUTRAL_500, NEUTRAL_900, Colors } from "./colors";
82
+ // Family app marks and their pure data (copied from `@vvedantb/ui/brand`), and `GoogleMark`.
83
+ export * from "./brand";
84
+ export { UpgradeToProCard } from "./marketing/UpgradeToProCard";
85
+ export { PricingModal } from "./marketing/PricingModal";
@@ -0,0 +1,4 @@
1
+ /** Up to `max` uppercase initials from a name: "Ada Lovelace" → "AL", " cher " → "C". Empty for a blank name. */
2
+ export function initials(name, max = 2) {
3
+ return name.trim().split(/\s+/).filter(Boolean).slice(0, max).map(word => Array.from(word)[0].toUpperCase()).join("");
4
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The web `LoadingState` `Drive` pixel grid (`packages/ui/src/ui/loading-state.tsx`)
3
+ * as plain numbers, so the timing stays testable apart from Reanimated. Each
4
+ * function is a worklet: the grid runs it on the UI thread every frame.
5
+ *
6
+ * Nine cells in a 3x3 grid pulse in a chevron wavefront that drives right.
7
+ * The 650 ms cycle is shorter than the sweep (0-360 ms of delays), so two
8
+ * fronts are always in flight.
9
+ */
10
+
11
+ /** One pulse of a cell, in ms: the web `PATTERNS.Drive.dur`. */
12
+ export const DRIVE_CYCLE_MS = 650;
13
+
14
+ /** A cell's opacity at the low and high keyframes. */
15
+ const LOW = 0.12;
16
+ const HIGH = 1;
17
+ /** A cell's still opacity before its delay ends: the web inline `opacity: 0.15`. */
18
+ export const DRIVE_IDLE_OPACITY = 0.15;
19
+
20
+ /**
21
+ * Each cell's start delay in ms, row by row: column plus distance from the
22
+ * middle row, times 90. The middle row leads, so the front is a chevron.
23
+ */
24
+ export const DRIVE_DELAYS = Array.from({
25
+ length: 9
26
+ }, (_, i) => {
27
+ const row = Math.floor(i / 3);
28
+ const column = i % 3;
29
+ return (column + Math.abs(row - 1)) * 90;
30
+ });
31
+
32
+ /** The web `size` tokens in points: `sm` is an 11 pt grid, `default` 15 pt. */
33
+ export const DRIVE_SIZES = {
34
+ sm: {
35
+ cell: 3,
36
+ gap: 1,
37
+ radius: 0.5
38
+ },
39
+ default: {
40
+ cell: 4,
41
+ gap: 1.5,
42
+ radius: 1
43
+ }
44
+ };
45
+ /**
46
+ * The frame held with Reduce Motion: mid-sweep, so the still grid reads as
47
+ * the chevron (left column bright, right column dim), not nine flat cells.
48
+ */
49
+ export const DRIVE_STILL_FRAME_MS = DRIVE_CYCLE_MS * 1.5;
50
+
51
+ /** CSS `ease-in-out`, `cubic-bezier(0.42, 0, 0.58, 1)`, at progress `x` (0-1). */
52
+ export function cssEaseInOut(x) {
53
+ "worklet";
54
+
55
+ if (x <= 0) return 0;
56
+ if (x >= 1) return 1;
57
+ // x(t) = 3(1-t)^2 t x1 + 3(1-t) t^2 x2 + t^3 rises with t, so bisect for t.
58
+ const curve = (t, p1, p2) => 3 * (1 - t) * (1 - t) * t * p1 + 3 * (1 - t) * t * t * p2 + t * t * t;
59
+ let lo = 0;
60
+ let hi = 1;
61
+ let t = x;
62
+ for (let i = 0; i < 20; i++) {
63
+ t = (lo + hi) / 2;
64
+ if (curve(t, 0.42, 0.58) < x) lo = t;else hi = t;
65
+ }
66
+ return curve(t, 0, 1);
67
+ }
68
+
69
+ /**
70
+ * A cell's opacity `elapsedMs` after the grid mounted, as the web WAAPI pulse:
71
+ * still at 0.15 during its delay, then 0.12 -> 1 -> 0.12 every cycle, each
72
+ * half eased `ease-in-out`.
73
+ */
74
+ export function driveCellOpacity(elapsedMs, delayMs) {
75
+ "worklet";
76
+
77
+ const local = elapsedMs - delayMs;
78
+ if (local < 0) return DRIVE_IDLE_OPACITY;
79
+ const progress = local % DRIVE_CYCLE_MS / DRIVE_CYCLE_MS;
80
+ return progress < 0.5 ? LOW + (HIGH - LOW) * cssEaseInOut(progress * 2) : HIGH - (HIGH - LOW) * cssEaseInOut((progress - 0.5) * 2);
81
+ }
@@ -0,0 +1,515 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { Children, cloneElement, Fragment, isValidElement, useContext } from "react";
3
+ import { StyleSheet, Text, View } from "react-native";
4
+ import { Renderer, useMarkdown } from "react-native-marked";
5
+ import { CodeBlock } from "../ai/code-block";
6
+ import { MONO_FONT, chatTextSize } from "../ai/chat-text";
7
+ import { ChatTextColorContext } from "../ai/response-text";
8
+ import { sizeStyle, trackingStyle, typeStyle } from "../controlStyles";
9
+ import { palette, withAlpha } from "../palette";
10
+ import { useLargerText } from "../theme";
11
+ import { tokens } from "../tokens";
12
+ import { fontFamily } from "../typography";
13
+ import { useScheme } from "../useScheme";
14
+ import { MARKDOWN_TABLE, MarkdownTable } from "./MarkdownTable";
15
+
16
+ /**
17
+ * Sizes from the web `markdown.css` (`@vvedantb/ui/tokens/css/markdown.css`),
18
+ * in em of the body size: h1 1.43, h2 1.29, h3 1.14, h4-h6 1; inline code
19
+ * 0.86. Headings line-height 1.3. Blocks, headings included, are 16 apart:
20
+ * Streamdown's root `space-y-4` (a utility) wins over the `.markdown` block
21
+ * margins (a component layer), so the web draws 1rem between every block.
22
+ */
23
+ export const MARKDOWN_SCALE = {
24
+ h1: 1.43,
25
+ h2: 1.29,
26
+ h3: 1.14,
27
+ h4: 1,
28
+ inlineCode: 0.86,
29
+ headingLineHeight: 1.3,
30
+ blockGap: 16
31
+ };
32
+ /**
33
+ * Markdown at chat size, styled as the web `.markdown` (Streamdown) block:
34
+ * compact headings, primary links underlined at 30%, inline code on a 6%
35
+ * foreground wash at radius 6, and fenced code as `CodeBlock` (the flat
36
+ * `surface-secondary` panel with a language and copy header). Text takes the
37
+ * enclosing `MessageContent` colour, like `ResponseText`.
38
+ *
39
+ * Needs the optional peer `react-native-marked` (it renders through
40
+ * `useMarkdown`, so it nests in a `ScrollView` without a `FlatList`).
41
+ */
42
+ export function Markdown(t0) {
43
+ const $ = _c(28);
44
+ const {
45
+ children,
46
+ onCopyCode,
47
+ onCopyTable,
48
+ leading: t1,
49
+ renderLink,
50
+ headings: t2
51
+ } = t0;
52
+ const leading = t1 === undefined ? "relaxed" : t1;
53
+ const headings = t2 === undefined ? "default" : t2;
54
+ const scheme = useScheme();
55
+ const largerText = useLargerText();
56
+ const inherited = useContext(ChatTextColorContext);
57
+ let t3;
58
+ let t4;
59
+ let t5;
60
+ let t6;
61
+ let t7;
62
+ let t8;
63
+ if ($[0] !== children || $[1] !== headings || $[2] !== inherited || $[3] !== largerText || $[4] !== leading || $[5] !== onCopyCode || $[6] !== onCopyTable || $[7] !== renderLink || $[8] !== scheme) {
64
+ const body = sizeStyle(chatTextSize("body", largerText), leading === "relaxed" ? "relaxed" : undefined);
65
+ const color = inherited ?? tokens.color.foreground[scheme];
66
+ const renderer = new NativeUiMarkdownRenderer(scheme, body.fontSize, color, onCopyCode, onCopyTable, renderLink);
67
+ t7 = useMarkdown;
68
+ t8 = children;
69
+ t3 = scheme;
70
+ t4 = renderer;
71
+ const t9 = palette.primary[scheme];
72
+ const t10 = tokens.color.surfaceSecondary[scheme];
73
+ const t11 = tokens.color.border[scheme];
74
+ if ($[15] !== color || $[16] !== t10 || $[17] !== t11 || $[18] !== t9) {
75
+ t5 = {
76
+ colors: {
77
+ text: color,
78
+ link: t9,
79
+ code: t10,
80
+ border: t11
81
+ }
82
+ };
83
+ $[15] = color;
84
+ $[16] = t10;
85
+ $[17] = t11;
86
+ $[18] = t9;
87
+ $[19] = t5;
88
+ } else {
89
+ t5 = $[19];
90
+ }
91
+ t6 = markdownStyles(scheme, body, color, headings);
92
+ $[0] = children;
93
+ $[1] = headings;
94
+ $[2] = inherited;
95
+ $[3] = largerText;
96
+ $[4] = leading;
97
+ $[5] = onCopyCode;
98
+ $[6] = onCopyTable;
99
+ $[7] = renderLink;
100
+ $[8] = scheme;
101
+ $[9] = t3;
102
+ $[10] = t4;
103
+ $[11] = t5;
104
+ $[12] = t6;
105
+ $[13] = t7;
106
+ $[14] = t8;
107
+ } else {
108
+ t3 = $[9];
109
+ t4 = $[10];
110
+ t5 = $[11];
111
+ t6 = $[12];
112
+ t7 = $[13];
113
+ t8 = $[14];
114
+ }
115
+ let t9;
116
+ if ($[20] !== t3 || $[21] !== t4 || $[22] !== t5 || $[23] !== t6) {
117
+ t9 = {
118
+ colorScheme: t3,
119
+ renderer: t4,
120
+ theme: t5,
121
+ styles: t6
122
+ };
123
+ $[20] = t3;
124
+ $[21] = t4;
125
+ $[22] = t5;
126
+ $[23] = t6;
127
+ $[24] = t9;
128
+ } else {
129
+ t9 = $[24];
130
+ }
131
+ const elements = t7(t8, t9);
132
+ let t10;
133
+ if ($[25] === Symbol.for("react.memo_cache_sentinel")) {
134
+ t10 = {
135
+ marginVertical: -MARKDOWN_SCALE.blockGap / 2
136
+ };
137
+ $[25] = t10;
138
+ } else {
139
+ t10 = $[25];
140
+ }
141
+ let t11;
142
+ if ($[26] !== elements) {
143
+ t11 = <View style={t10}>{elements}</View>;
144
+ $[26] = elements;
145
+ $[27] = t11;
146
+ } else {
147
+ t11 = $[27];
148
+ }
149
+ return t11;
150
+ }
151
+
152
+ /** The style map `useMarkdown` takes, from the web scale. Exported for tests. */
153
+ export function markdownStyles(scheme, body, color, headings = "default") {
154
+ const label = {
155
+ fontFamily: fontFamily.medium,
156
+ ...typeStyle("medium"),
157
+ ...sizeStyle("text-xs"),
158
+ ...trackingStyle(12, "wide"),
159
+ textTransform: "uppercase",
160
+ color: tokens.color.muted[scheme],
161
+ // With the half gap each block keeps (margins do not collapse): 16 above,
162
+ // the next block's 8 below (the web `mt-4 mb-1.5`, collapsed).
163
+ marginTop: MARKDOWN_SCALE.blockGap / 2,
164
+ marginBottom: 0,
165
+ paddingBottom: 0,
166
+ borderBottomWidth: 0
167
+ };
168
+ const scaled = scale => ({
169
+ fontFamily: fontFamily.semibold,
170
+ fontWeight: "600",
171
+ fontSize: Math.round(body.fontSize * scale),
172
+ lineHeight: Math.round(body.fontSize * scale * MARKDOWN_SCALE.headingLineHeight),
173
+ letterSpacing: -0.01 * body.fontSize * scale,
174
+ color,
175
+ marginVertical: MARKDOWN_SCALE.blockGap / 2,
176
+ // `react-native-marked` rules h1 and h2 off; the web has no rule.
177
+ paddingBottom: 0,
178
+ borderBottomWidth: 0
179
+ });
180
+ const heading = scale => headings === "label" ? label : scaled(scale);
181
+ const gap = {
182
+ marginVertical: MARKDOWN_SCALE.blockGap / 2
183
+ };
184
+ // The library's inline styles start from its own 16 / 24 text, so each one
185
+ // restates the body size, or a bold or linked word grows the line.
186
+ const inline = {
187
+ fontSize: body.fontSize,
188
+ lineHeight: body.lineHeight
189
+ };
190
+ return {
191
+ text: {
192
+ fontFamily: fontFamily.regular,
193
+ ...inline,
194
+ color
195
+ },
196
+ paragraph: {
197
+ ...gap,
198
+ paddingVertical: 0
199
+ },
200
+ strong: {
201
+ ...inline,
202
+ fontFamily: fontFamily.semibold,
203
+ fontWeight: "600"
204
+ },
205
+ em: {
206
+ ...inline,
207
+ fontStyle: "italic"
208
+ },
209
+ strikethrough: {
210
+ ...inline,
211
+ textDecorationLine: "line-through"
212
+ },
213
+ link: {
214
+ ...inline,
215
+ fontStyle: "normal",
216
+ color: palette.primary[scheme],
217
+ textDecorationLine: "underline",
218
+ textDecorationColor: withAlpha(palette.primary[scheme], 0.3)
219
+ },
220
+ h1: heading(MARKDOWN_SCALE.h1),
221
+ h2: heading(MARKDOWN_SCALE.h2),
222
+ h3: heading(MARKDOWN_SCALE.h3),
223
+ h4: heading(MARKDOWN_SCALE.h4),
224
+ h5: heading(MARKDOWN_SCALE.h4),
225
+ h6: {
226
+ ...heading(MARKDOWN_SCALE.h4),
227
+ color: tokens.color.muted[scheme]
228
+ },
229
+ li: {
230
+ fontFamily: fontFamily.regular,
231
+ fontSize: body.fontSize,
232
+ lineHeight: body.lineHeight,
233
+ color
234
+ },
235
+ blockquote: {
236
+ ...gap,
237
+ paddingLeft: 13,
238
+ borderLeftWidth: 2,
239
+ borderLeftColor: tokens.color.border[scheme],
240
+ backgroundColor: "transparent",
241
+ opacity: 1
242
+ },
243
+ hr: {
244
+ ...gap,
245
+ height: 1,
246
+ borderBottomWidth: 0,
247
+ backgroundColor: tokens.color.border[scheme]
248
+ },
249
+ codespan: inlineCodeStyle(scheme, body.fontSize, color)
250
+ };
251
+ }
252
+
253
+ /**
254
+ * Web `.markdown-inline-code`: 0.86em mono on a 6% foreground wash, radius 6,
255
+ * in the text colour (`color`, else the platform default).
256
+ */
257
+ export function inlineCodeStyle(scheme, fontSize, color) {
258
+ return {
259
+ fontFamily: MONO_FONT,
260
+ fontStyle: "normal",
261
+ fontSize: Math.round(fontSize * MARKDOWN_SCALE.inlineCode),
262
+ backgroundColor: withAlpha(tokens.color.foreground[scheme], 0.06),
263
+ borderRadius: 6,
264
+ ...(color ? {
265
+ color
266
+ } : null)
267
+ };
268
+ }
269
+
270
+ /**
271
+ * A paragraph block. A component of its own so `listItem` can find it in a
272
+ * loose list (the parser's own paragraph is a plain `View`).
273
+ */
274
+ function Paragraph(t0) {
275
+ const $ = _c(3);
276
+ const {
277
+ style,
278
+ children
279
+ } = t0;
280
+ let t1;
281
+ if ($[0] !== children || $[1] !== style) {
282
+ t1 = <View style={style}>{children}</View>;
283
+ $[0] = children;
284
+ $[1] = style;
285
+ $[2] = t1;
286
+ } else {
287
+ t1 = $[2];
288
+ }
289
+ return t1;
290
+ }
291
+ /**
292
+ * An inline-code chip (see `codespan`). A component of its own so
293
+ * `restyleText` can find it: NativeWind's JSX swaps a `Text` made in this
294
+ * file for its own wrapper, so the type no longer equals `Text`.
295
+ */
296
+ function InlineCode(t0) {
297
+ const $ = _c(3);
298
+ const {
299
+ style,
300
+ children
301
+ } = t0;
302
+ let t1;
303
+ if ($[0] !== children || $[1] !== style) {
304
+ t1 = <Text selectable={true} style={style}>{children}</Text>;
305
+ $[0] = children;
306
+ $[1] = style;
307
+ $[2] = t1;
308
+ } else {
309
+ t1 = $[2];
310
+ }
311
+ return t1;
312
+ }
313
+
314
+ /**
315
+ * `node` with `map` applied to the style of every `Text` in it. The parser
316
+ * bakes the text style into each `Text`, and React Native text takes no
317
+ * style from a parent view, so a block that restyles its text (a quote, a
318
+ * table) rewrites it here. Links keep their own style.
319
+ */
320
+ function restyleText(node, map) {
321
+ return Children.map(node, child => {
322
+ if (!isValidElement(child)) return child;
323
+ if (child.props.accessibilityRole === "link") return child;
324
+ const children = child.props.children === undefined ? undefined : restyleText(child.props.children, map);
325
+ if (child.type === Text || child.type === InlineCode) {
326
+ return cloneElement(child, {
327
+ style: map(StyleSheet.flatten(child.props.style) ?? {})
328
+ }, children);
329
+ }
330
+ return children === undefined ? child : cloneElement(child, {}, children);
331
+ });
332
+ }
333
+
334
+ /** A link's text with its styling stripped: the strings in it, in order. */
335
+ function plainText(node) {
336
+ let out = "";
337
+ Children.forEach(node, child => {
338
+ if (typeof child === "string" || typeof child === "number") out += String(child);else if (isValidElement(child)) out += plainText(child.props.children);
339
+ });
340
+ return out;
341
+ }
342
+
343
+ /**
344
+ * A cell's plain text, as the web reads a cell's `textContent` for Copy
345
+ * table: inline code without the no-break spaces that pad its chip.
346
+ */
347
+ function cellText(cell) {
348
+ return nodeText(cell).trim();
349
+ }
350
+ function nodeText(node) {
351
+ return Children.toArray(node).map(child => {
352
+ if (typeof child === "string" || typeof child === "number") return String(child);
353
+ if (!isValidElement(child)) return "";
354
+ const text = nodeText(child.props.children);
355
+ return child.type === InlineCode ? text.replace(/^\u00A0|\u00A0$/g, "") : text;
356
+ }).join("");
357
+ }
358
+
359
+ /** Web `.markdown` lists: 1.25rem (20) indent, 0.25rem (4) between items, muted markers (a 5-wide disc). */
360
+ export const MARKDOWN_LIST = {
361
+ indent: 20,
362
+ itemGap: 4,
363
+ disc: 5
364
+ };
365
+
366
+ /**
367
+ * `react-native-marked`'s renderer with native-ui code and lists: `CodeBlock`
368
+ * panels, inline-code chips, and lists drawn as the web `.markdown` lists
369
+ * (the library's own markers apply its marker style to every item). Links,
370
+ * quotes and tables are drawn here too.
371
+ */
372
+ class NativeUiMarkdownRenderer extends Renderer {
373
+ constructor(scheme, fontSize, color, onCopyCode, onCopyTable, renderLink) {
374
+ super();
375
+ this.scheme = scheme;
376
+ this.fontSize = fontSize;
377
+ this.color = color;
378
+ this.onCopyCode = onCopyCode;
379
+ this.onCopyTable = onCopyTable;
380
+ this.renderLink = renderLink;
381
+ }
382
+
383
+ /**
384
+ * Web `.markdown-link`, medium weight. The parser gives a link the
385
+ * paragraph's regular face, so the face is set here, on the link and the
386
+ * text in it.
387
+ */
388
+ link(children, href, styles, title) {
389
+ const custom = this.renderLink?.(href, plainText(children));
390
+ if (custom !== undefined) return <Fragment key={this.getKey()}>{custom}</Fragment>;
391
+ const medium = style => ({
392
+ ...style,
393
+ fontFamily: fontFamily.medium,
394
+ ...typeStyle("medium")
395
+ });
396
+ const content = typeof children === "string" ? children : Children.toArray(restyleText(children, medium));
397
+ return super.link(content, href, medium(styles ?? {}), title);
398
+ }
399
+
400
+ /** Web `.markdown blockquote`: the left rule, and the text in the muted colour. */
401
+ blockquote(children, styles) {
402
+ const muted = tokens.color.muted[this.scheme];
403
+ // The inner blocks' outer margins come off, as web margins collapse into the quote's.
404
+ return <View key={this.getKey()} style={styles}>
405
+ <View style={{
406
+ marginVertical: -MARKDOWN_SCALE.blockGap / 2
407
+ }}>
408
+ {restyleText(children, style => ({
409
+ ...style,
410
+ color: muted
411
+ }))}
412
+ </View>
413
+ </View>;
414
+ }
415
+
416
+ /**
417
+ * The web table on its panel (`MarkdownTable`): header cells semibold,
418
+ * rows split by a 70% border, and the table actions above it.
419
+ */
420
+ table(header, rows) {
421
+ const {
422
+ fontScale,
423
+ lineHeight
424
+ } = MARKDOWN_TABLE;
425
+ const small = style => {
426
+ const size = typeof style.fontSize === "number" ? style.fontSize * fontScale : this.fontSize * fontScale;
427
+ // Body text takes the table line height; inline code keeps its own.
428
+ return style.fontFamily === MONO_FONT ? {
429
+ ...style,
430
+ fontSize: size
431
+ } : {
432
+ ...style,
433
+ fontSize: size,
434
+ lineHeight: size * lineHeight
435
+ };
436
+ };
437
+ const semibold = style => ({
438
+ ...small(style),
439
+ fontFamily: fontFamily.semibold,
440
+ ...typeStyle("semibold")
441
+ });
442
+ return <View key={this.getKey()} style={{
443
+ marginVertical: MARKDOWN_SCALE.blockGap / 2
444
+ }}>
445
+ <MarkdownTable header={header.map(cell => restyleText(cell, semibold))} rows={rows.map(cells => cells.map(cell => restyleText(cell, small)))} data={{
446
+ headers: header.map(cellText),
447
+ rows: rows.map(cells => cells.map(cellText))
448
+ }} color={this.color} onCopy={this.onCopyTable} />
449
+ </View>;
450
+ }
451
+ paragraph(children, styles) {
452
+ return <Paragraph key={this.getKey()} style={styles}>
453
+ {children}
454
+ </Paragraph>;
455
+ }
456
+
457
+ /**
458
+ * A loose list (items a blank line apart, as Eva's doc editor writes them)
459
+ * puts each item's text in a paragraph. Its outer margins come off, as in
460
+ * a quote, so the text sits on the marker's line, as the tight list.
461
+ */
462
+ listItem(children, styles) {
463
+ const loose = children.some(child => isValidElement(child) && child.type === Paragraph);
464
+ if (!loose) return super.listItem(children, styles);
465
+ return <View key={this.getKey()} style={styles}>
466
+ <View style={{
467
+ marginVertical: -MARKDOWN_SCALE.blockGap / 2
468
+ }}>{children}</View>
469
+ </View>;
470
+ }
471
+ code(text, language) {
472
+ return <CodeBlock key={this.getKey()} variant="markdown" code={text} language={language || undefined} onCopy={this.onCopyCode} />;
473
+ }
474
+ codespan(text) {
475
+ // Text in Text takes no padding: no-break spaces pad the chip and keep it on one line.
476
+ return <InlineCode key={this.getKey()} style={inlineCodeStyle(this.scheme, this.fontSize, this.color)}>
477
+ {`\u00A0${text}\u00A0`}
478
+ </InlineCode>;
479
+ }
480
+ list(ordered, li, _listStyle, textStyle, startIndex = 1) {
481
+ const muted = tokens.color.muted[this.scheme];
482
+ const lineHeight = typeof textStyle?.lineHeight === "number" ? textStyle.lineHeight : 24;
483
+ // A drawn disc: the font's bullet glyph is far smaller than the web `disc` marker.
484
+ const disc = MARKDOWN_LIST.disc;
485
+ return <View key={this.getKey()} style={{
486
+ marginVertical: MARKDOWN_SCALE.blockGap / 2,
487
+ gap: MARKDOWN_LIST.itemGap
488
+ }}>
489
+ {li.map((item, index) => <View key={index} style={{
490
+ flexDirection: "row"
491
+ }}>
492
+ {ordered ? <Text style={[textStyle, {
493
+ width: MARKDOWN_LIST.indent,
494
+ color: muted,
495
+ fontVariant: ["tabular-nums"]
496
+ }]}>
497
+ {`${startIndex + index}.`}
498
+ </Text> : <View style={{
499
+ width: MARKDOWN_LIST.indent,
500
+ paddingTop: (lineHeight - disc) / 2
501
+ }}>
502
+ <View style={{
503
+ width: disc,
504
+ height: disc,
505
+ borderRadius: disc / 2,
506
+ backgroundColor: muted
507
+ }} />
508
+ </View>}
509
+ <View style={{
510
+ flex: 1
511
+ }}>{item}</View>
512
+ </View>)}
513
+ </View>;
514
+ }
515
+ }