@vvedantb/native-ui 0.12.1 → 0.17.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 (113) hide show
  1. package/README.md +82 -19
  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/card-button.tsx +49 -0
  36. package/src/ai/chat-text.ts +13 -7
  37. package/src/ai/chat-thread.tsx +34 -27
  38. package/src/ai/choice-question.tsx +154 -35
  39. package/src/ai/code-block.tsx +189 -49
  40. package/src/ai/collapsible.tsx +19 -13
  41. package/src/ai/conversation.tsx +390 -57
  42. package/src/ai/icon-button.tsx +41 -6
  43. package/src/ai/index.ts +52 -1
  44. package/src/ai/message-action-sheet.tsx +168 -0
  45. package/src/ai/message-quote.tsx +98 -0
  46. package/src/ai/message.tsx +226 -31
  47. package/src/ai/prompt-input-menu.tsx +197 -0
  48. package/src/ai/prompt-input.tsx +308 -73
  49. package/src/ai/reply-quote-chip.tsx +90 -0
  50. package/src/ai/shimmer.tsx +4 -6
  51. package/src/ai/subagent-cards.tsx +209 -0
  52. package/src/ai/tool-approval-card.tsx +162 -0
  53. package/src/ai/trace.tsx +54 -17
  54. package/src/ai/unified-diff.tsx +182 -0
  55. package/src/auth/GoogleSignInButton.tsx +6 -3
  56. package/src/auth/SignInScreen.tsx +11 -2
  57. package/src/brand/ProviderIcon.tsx +136 -0
  58. package/src/brand/index.ts +2 -0
  59. package/src/configureTheme.ts +39 -11
  60. package/src/controlStyles.ts +46 -30
  61. package/src/entries/gesture-handler.ts +4 -0
  62. package/src/entries/keyboard-controller.ts +4 -0
  63. package/src/entries/markdown.ts +1 -1
  64. package/src/entries/primitives.ts +24 -3
  65. package/src/entries/theme.ts +2 -1
  66. package/src/entries/video.ts +4 -0
  67. package/src/entries/webview.ts +6 -0
  68. package/src/floating.ts +25 -4
  69. package/src/haptics.ts +30 -4
  70. package/src/highlight.ts +302 -1
  71. package/src/index.ts +27 -5
  72. package/src/loadingGrid.ts +75 -0
  73. package/src/markdown/Markdown.tsx +234 -32
  74. package/src/markdown/MarkdownTable.tsx +188 -0
  75. package/src/markdown/tableText.ts +58 -0
  76. package/src/marketing/UpgradeToProCard.tsx +4 -3
  77. package/src/media/ImageGallery.tsx +224 -0
  78. package/src/media/MediaCard.tsx +100 -0
  79. package/src/media/imageGrid.ts +51 -0
  80. package/src/motion.ts +33 -0
  81. package/src/pagerSwipe.ts +102 -0
  82. package/src/palette.ts +8 -1
  83. package/src/primitives/button.tsx +4 -1
  84. package/src/primitives/keyboard-controller.tsx +106 -0
  85. package/src/primitives/keyboard-parts.tsx +104 -0
  86. package/src/primitives/overlay.tsx +133 -45
  87. package/src/primitives/pager-gesture.tsx +131 -0
  88. package/src/primitives/sheet-drag.tsx +305 -0
  89. package/src/primitives/sheet-gesture.tsx +181 -0
  90. package/src/primitives/swipe-row-gesture.tsx +290 -0
  91. package/src/primitives/toast-drag-gesture.tsx +92 -0
  92. package/src/primitives/web-click-guard.ts +50 -0
  93. package/src/relativeTime.ts +94 -0
  94. package/src/router/RouterStackPageHeader.tsx +2 -2
  95. package/src/settings/SettingsEmptyState.tsx +20 -10
  96. package/src/settings/SettingsField.tsx +11 -5
  97. package/src/settings/SettingsSection.tsx +8 -5
  98. package/src/settings/SettingsToggleRow.tsx +55 -22
  99. package/src/settings/relaxedSizeStyle.ts +11 -0
  100. package/src/sheetDrag.ts +67 -0
  101. package/src/swipeRow.ts +169 -0
  102. package/src/tabBarRoutes.ts +18 -0
  103. package/src/tabIndicator.ts +71 -0
  104. package/src/tailwind-preset.cjs +13 -6
  105. package/src/tailwind-preset.d.cts +5 -0
  106. package/src/theme.tsx +6 -2
  107. package/src/toastDrag.ts +58 -0
  108. package/src/typography.ts +20 -3
  109. package/src/useScheme.ts +10 -1
  110. package/src/video/InlineVideo.tsx +91 -0
  111. package/src/webview/HtmlView.tsx +91 -0
  112. package/src/webview/HtmlView.web.tsx +21 -0
  113. package/src/webview/htmlView.ts +45 -0
@@ -10,17 +10,44 @@ import { useScheme } from "../useScheme";
10
10
  /** The web `icon-sm` button: 32 square with a 16 icon. */
11
11
  export const CHAT_ICON_BUTTON_SIZE = 32;
12
12
 
13
+ /**
14
+ * Side and icon per size: `sm` is the web `icon-sm` (32, 16 icon), `xs` the
15
+ * web `icon-xs` (28, 14 icon) and `md` the web `size-9` (36, 16 icon), Eva's
16
+ * composer send button. `touch` has no web twin: a 44 square with a 16 icon,
17
+ * for a row of ghost buttons a thumb taps (the message actions), where the
18
+ * web 28 squares 2 apart are a mouse's density.
19
+ */
20
+ const CHAT_ICON_BUTTON = {
21
+ xs: { side: 28, icon: 14, className: "h-7 w-7" },
22
+ sm: { side: CHAT_ICON_BUTTON_SIZE, icon: 16, className: "h-8 w-8" },
23
+ md: { side: 36, icon: 16, className: "h-9 w-9" },
24
+ touch: { side: MIN_TOUCH_TARGET, icon: 16, className: "h-11 w-11" },
25
+ } as const;
26
+
27
+ export type ChatIconButtonSize = keyof typeof CHAT_ICON_BUTTON;
28
+
13
29
  export type ChatIconButtonProps = Omit<ButtonProps, "children" | "size" | "icon"> & {
14
30
  icon: ComponentType<IconProps>;
15
31
  /** Names the button for VoiceOver and TalkBack; it has no text. */
16
32
  label: string;
17
33
  /** Default: the variant's label colour. */
18
34
  iconColor?: string;
35
+ /** Default `sm`. Larger text paints each 44 with a 20 icon. */
36
+ size?: ChatIconButtonSize;
37
+ /**
38
+ * `"square"` (default) slops to 44 on every side. `"vertical"` slops only
39
+ * the height to 44, for a dense row (the message actions sit 2 to 4 apart),
40
+ * where sideways slop would overlap the next button and steal its taps.
41
+ * `{ left, right }` is `"vertical"` plus that much on each side, for the
42
+ * outer side of a button at the end of such a row.
43
+ */
44
+ slop?: "square" | "vertical" | { left: number; right: number };
19
45
  };
20
46
 
21
47
  /**
22
- * A round `Button` at the web `icon-sm` size (32), with the slop to 44. With
23
- * larger text it is painted 44 with a 20 icon. The message
48
+ * A round `Button` at the web `icon-sm` size (32), or another
49
+ * `ChatIconButtonSize`, with the slop to 44. With larger text it is painted
50
+ * 44 with a 20 icon. The message
24
51
  * actions, the composer tools and its send button, and the code copy button.
25
52
  * Round from the `Button` pill default; `shape="rounded"` gives radius 16.
26
53
  */
@@ -29,23 +56,31 @@ export function ChatIconButton({
29
56
  label,
30
57
  iconColor,
31
58
  variant = "ghost",
59
+ size = "sm",
60
+ slop = "square",
32
61
  className,
33
62
  ...props
34
63
  }: ChatIconButtonProps) {
35
64
  const scheme = useScheme();
36
65
  const largerText = useLargerText();
37
- const size = largerText ? MIN_TOUCH_TARGET : CHAT_ICON_BUTTON_SIZE;
66
+ const metrics = largerText
67
+ ? { side: MIN_TOUCH_TARGET, icon: 20, className: "h-11 w-11" }
68
+ : CHAT_ICON_BUTTON[size];
38
69
  const color = iconColor ?? buttonColors(variant, scheme)?.color ?? tokens.color.foreground[scheme];
39
70
  return (
40
71
  <Button
41
72
  variant={variant}
42
73
  size="icon"
43
74
  accessibilityLabel={label}
44
- hitSlop={touchSlop(size)}
45
- className={cn(largerText ? "h-11 w-11" : "h-8 w-8", className)}
75
+ hitSlop={
76
+ slop === "square"
77
+ ? touchSlop(metrics.side)
78
+ : { ...touchSlop(MIN_TOUCH_TARGET, metrics.side), ...(slop === "vertical" ? null : slop) }
79
+ }
80
+ className={cn(metrics.className, className)}
46
81
  {...props}
47
82
  >
48
- <Icon size={largerText ? 20 : 16} color={color} />
83
+ <Icon size={metrics.icon} color={color} />
49
84
  </Button>
50
85
  );
51
86
  }
package/src/ai/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // `@vvedantb/native-ui/ai` entry only, so apps without a chat do not bundle
3
3
  // them. Generic: no model names and no app logic.
4
4
  export { useStreamingDisclosure } from "./use-streaming-disclosure";
5
- export { chatTextSize, formatElapsed, MONO_FONT } from "./chat-text";
5
+ export { chatTextSize, formatElapsed, META_TEXT_STYLE, MONO_FONT } from "./chat-text";
6
6
  export type { ChatTextKind } from "./chat-text";
7
7
  export { Shimmer, SHIMMER_LOW_OPACITY } from "./shimmer";
8
8
  export type { ShimmerProps } from "./shimmer";
@@ -17,8 +17,10 @@ export type { CollapsibleProps, CollapsibleTriggerProps } from "./collapsible";
17
17
  export {
18
18
  Conversation,
19
19
  ConversationContent,
20
+ ConversationList,
20
21
  ConversationEmptyState,
21
22
  ConversationScrollButton,
23
+ ConversationDock,
22
24
  useConversation,
23
25
  isScrolledToBottom,
24
26
  AT_BOTTOM_THRESHOLD,
@@ -26,8 +28,11 @@ export {
26
28
  export type {
27
29
  ConversationProps,
28
30
  ConversationContentProps,
31
+ ConversationListProps,
32
+ ConversationScroller,
29
33
  ConversationEmptyStateProps,
30
34
  ConversationScrollButtonProps,
35
+ ConversationDockProps,
31
36
  } from "./conversation";
32
37
  export {
33
38
  Message,
@@ -35,7 +40,9 @@ export {
35
40
  MessageActionsBar,
36
41
  MessageAction,
37
42
  MessageMeta,
43
+ MessageBotLabel,
38
44
  MESSAGE_ACTIONS,
45
+ MESSAGE_HIGHLIGHT_MS,
39
46
  userBubbleStyle,
40
47
  tintBubbleStyle,
41
48
  } from "./message";
@@ -46,9 +53,17 @@ export type {
46
53
  MessageBubble,
47
54
  MessageActionsBarProps,
48
55
  MessageMetaProps,
56
+ MessageBot,
57
+ MessageBotLabelProps,
49
58
  MessageActionKind,
50
59
  MessageActionProps,
51
60
  } from "./message";
61
+ export { MessageActionSheet } from "./message-action-sheet";
62
+ export type { MessageActionSheetProps, MessageSheetAction } from "./message-action-sheet";
63
+ export { ReplyQuoteChip } from "./reply-quote-chip";
64
+ export type { ReplyQuoteChipProps } from "./reply-quote-chip";
65
+ export { MessageQuote, QUOTE_MAX_LINES } from "./message-quote";
66
+ export type { MessageQuoteProps } from "./message-quote";
52
67
  export {
53
68
  Reasoning,
54
69
  ReasoningTrigger,
@@ -92,18 +107,48 @@ export type {
92
107
  } from "./chain-of-thought";
93
108
  export { ActivityStep } from "./activity-step";
94
109
  export type { ActivityStepProps, ActivityStepStatus } from "./activity-step";
110
+ // The web timeline's pure logic (byte-identical copies) and its folded rules.
111
+ // The step type is `ActivityTimelineStep` here: `ActivityStep` is the line above.
112
+ export { activityStepHasRichDetail } from "./activity-step-types";
113
+ export type {
114
+ ActivityStep as ActivityTimelineStep,
115
+ ActivityStepEdit,
116
+ ActivityStepOutput,
117
+ ActivityTodoItem,
118
+ } from "./activity-step-types";
119
+ export {
120
+ deriveActionGroupSummary,
121
+ deriveReadableCommandDisplay,
122
+ deriveStepRowPresentation,
123
+ resolveCommandVisualKind,
124
+ } from "./activity-step-label";
125
+ export type { CommandVisualKind, StepRowPresentation } from "./activity-step-label";
126
+ export { activityRowKey, activitySegmentKey, buildActivityRows, groupActivityRows } from "./activity-tasks-utils";
127
+ export type { ActivityRow, ActivitySegment } from "./activity-tasks-utils";
128
+ export {
129
+ ACTIVITY_MAX_VISIBLE_BLOCKS,
130
+ ACTIVITY_MAX_VISIBLE_CHILDREN,
131
+ activityChildRows,
132
+ activityStepSeconds,
133
+ foldedActivitySegments,
134
+ } from "./activity-timeline";
95
135
  export {
96
136
  PromptInput,
97
137
  PromptInputButton,
138
+ PromptInputChip,
98
139
  PromptInputSubmit,
140
+ PROMPT_INPUT_CHIP,
99
141
  isGenerating,
100
142
  } from "./prompt-input";
101
143
  export type {
102
144
  ChatStatus,
145
+ PromptInputChipProps,
103
146
  PromptInputProps,
104
147
  PromptInputButtonProps,
105
148
  PromptInputSubmitProps,
106
149
  } from "./prompt-input";
150
+ export { PromptInputMenu, PromptInputMenuItem, PROMPT_INPUT_MENU, promptInputMenuHeight } from "./prompt-input-menu";
151
+ export type { PromptInputMenuProps, PromptInputMenuItemProps } from "./prompt-input-menu";
107
152
  export { Suggestions, Suggestion } from "./suggestion";
108
153
  export type { SuggestionsProps, SuggestionProps } from "./suggestion";
109
154
  export { Sources, SourcesTrigger, SourcesContent, Source, sourcesLabel } from "./sources";
@@ -136,3 +181,9 @@ export { ChoiceQuestion, choiceAnswers } from "./choice-question";
136
181
  export type { ChoiceQuestionProps, ChoiceQuestionItem } from "./choice-question";
137
182
  export { ActivityStepDetail } from "./activity-step-detail";
138
183
  export type { ActivityStepDetailProps, ActivityStepDetailStep } from "./activity-step-detail";
184
+ export { UnifiedDiff, DiffStat, UNIFIED_DIFF } from "./unified-diff";
185
+ export type { UnifiedDiffProps, UnifiedDiffLine, DiffStatProps } from "./unified-diff";
186
+ export { ToolApprovalCard, ToolDraftCard, summarizeArgs, ARGS_MAX_CHARS } from "./tool-approval-card";
187
+ export type { ToolApprovalCardProps, ToolDraftCardProps, ToolDecision } from "./tool-approval-card";
188
+ export { SubagentRunCard, SubagentResultCard, subagentStatusColors } from "./subagent-cards";
189
+ export type { SubagentRunCardProps, SubagentResultCardProps, SubagentStatus } from "./subagent-cards";
@@ -0,0 +1,168 @@
1
+ import { useEffect, type ComponentType } from "react";
2
+ import { Modal, Pressable, StyleSheet, Text, View } from "react-native";
3
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
4
+ import type { IconProps } from "@tabler/icons-react-native";
5
+ import { cn } from "../cn";
6
+ import { MIN_TOUCH_TARGET, sizeStyle, textStyle, touchSlop, typeStyle } from "../controlStyles";
7
+ import { selectionHaptic } from "../haptics";
8
+ import { palette } from "../palette";
9
+ import { useLargerText } from "../theme";
10
+ import { tokens } from "../tokens";
11
+ import { useReduceMotion } from "../useReduceMotion";
12
+ import { useScheme } from "../useScheme";
13
+ import { chatTextSize } from "./chat-text";
14
+
15
+ /** One row of `MessageActionSheet`. */
16
+ export type MessageSheetAction = {
17
+ key: string;
18
+ label: string;
19
+ icon?: ComponentType<IconProps>;
20
+ /** Draws the row in the danger colour, such as Delete. */
21
+ destructive?: boolean;
22
+ onPress: () => void;
23
+ };
24
+
25
+ /** The sheet's default title, also the message's `longpress` action label. */
26
+ export const MESSAGE_ACTIONS_TITLE = "Message actions";
27
+
28
+ export type MessageActionSheetProps = {
29
+ open: boolean;
30
+ /** Called on a backdrop tap, Cancel, Android back and after an action runs. */
31
+ onClose: () => void;
32
+ actions: MessageSheetAction[];
33
+ /** Shown above the rows, and names the sheet for VoiceOver and TalkBack. Default "Message actions". */
34
+ title?: string;
35
+ /** Default "Cancel". */
36
+ cancelLabel?: string;
37
+ testID?: string;
38
+ };
39
+
40
+ /**
41
+ * The sheet that opens on a long press of a chat message: a bottom sheet in
42
+ * an RN `Modal` (no `@gorhom/bottom-sheet`) with one pressable row per action
43
+ * and a Cancel row. It closes on the backdrop, Cancel, Android back and after
44
+ * an action. A `selectionHaptic()` fires when it opens (`expo-haptics` is an
45
+ * optional peer). Row testIDs are `{testID}-action-{key}`.
46
+ */
47
+ export function MessageActionSheet({
48
+ open,
49
+ onClose,
50
+ actions,
51
+ title = MESSAGE_ACTIONS_TITLE,
52
+ cancelLabel = "Cancel",
53
+ testID = "message-action-sheet",
54
+ }: MessageActionSheetProps) {
55
+ const scheme = useScheme();
56
+ const largerText = useLargerText();
57
+ const insets = useSafeAreaInsets();
58
+ const reduceMotion = useReduceMotion();
59
+
60
+ useEffect(() => {
61
+ if (open) selectionHaptic();
62
+ }, [open]);
63
+
64
+ const caption = chatTextSize("caption", largerText);
65
+ const body = chatTextSize("body", largerText);
66
+ const minHeight = largerText ? 56 : MIN_TOUCH_TARGET + 4;
67
+
68
+ return (
69
+ <Modal
70
+ visible={open}
71
+ transparent
72
+ animationType={reduceMotion ? "none" : "fade"}
73
+ statusBarTranslucent
74
+ onRequestClose={onClose}
75
+ >
76
+ <Pressable
77
+ testID={`${testID}-backdrop`}
78
+ accessibilityRole="button"
79
+ accessibilityLabel="Close"
80
+ onPress={onClose}
81
+ hitSlop={touchSlop(MIN_TOUCH_TARGET)}
82
+ style={[StyleSheet.absoluteFill, { backgroundColor: palette.backdrop[scheme] }]}
83
+ />
84
+ <View pointerEvents="box-none" style={styles.anchor}>
85
+ <View
86
+ testID={testID}
87
+ accessibilityViewIsModal
88
+ accessibilityLabel={title}
89
+ onAccessibilityEscape={onClose}
90
+ className="w-full self-center px-2"
91
+ style={{ maxWidth: 480, paddingBottom: insets.bottom + 8 }}
92
+ >
93
+ <View
94
+ className="overflow-hidden"
95
+ style={{ backgroundColor: tokens.color.surface[scheme], borderRadius: tokens.radius.card }}
96
+ >
97
+ <Text
98
+ accessibilityRole="header"
99
+ numberOfLines={1}
100
+ className={cn(caption, "px-4 pb-1 pt-3 font-instrument-sans-medium")}
101
+ style={[sizeStyle(caption), typeStyle("medium"), textStyle("muted", scheme)]}
102
+ >
103
+ {title}
104
+ </Text>
105
+ {actions.map((action, index) => {
106
+ const Icon = action.icon;
107
+ const color = action.destructive ? palette.danger[scheme] : tokens.color.foreground[scheme];
108
+ return (
109
+ <Pressable
110
+ key={action.key}
111
+ testID={`${testID}-action-${action.key}`}
112
+ accessibilityRole="button"
113
+ accessibilityLabel={action.label}
114
+ hitSlop={touchSlop(MIN_TOUCH_TARGET)}
115
+ onPress={() => {
116
+ onClose();
117
+ action.onPress();
118
+ }}
119
+ className="flex-row items-center gap-3 px-4"
120
+ style={[
121
+ { minHeight },
122
+ index > 0 && {
123
+ borderTopWidth: StyleSheet.hairlineWidth,
124
+ borderTopColor: tokens.color.separator[scheme],
125
+ },
126
+ ]}
127
+ >
128
+ {Icon ? <Icon size={largerText ? 22 : 20} color={color} /> : null}
129
+ <Text
130
+ numberOfLines={1}
131
+ className={cn(body, "min-w-0 flex-1 font-instrument-sans-medium")}
132
+ style={[sizeStyle(body), typeStyle("medium"), { color }]}
133
+ >
134
+ {action.label}
135
+ </Text>
136
+ </Pressable>
137
+ );
138
+ })}
139
+ </View>
140
+ <Pressable
141
+ testID={`${testID}-cancel`}
142
+ accessibilityRole="button"
143
+ accessibilityLabel={cancelLabel}
144
+ onPress={onClose}
145
+ hitSlop={touchSlop(MIN_TOUCH_TARGET)}
146
+ className="mt-2 items-center justify-center"
147
+ style={{
148
+ minHeight,
149
+ backgroundColor: tokens.color.surface[scheme],
150
+ borderRadius: tokens.radius.card,
151
+ }}
152
+ >
153
+ <Text
154
+ className={cn(body, "font-instrument-sans-semibold")}
155
+ style={[sizeStyle(body), typeStyle("semibold"), textStyle("foreground", scheme)]}
156
+ >
157
+ {cancelLabel}
158
+ </Text>
159
+ </Pressable>
160
+ </View>
161
+ </View>
162
+ </Modal>
163
+ );
164
+ }
165
+
166
+ const styles = StyleSheet.create({
167
+ anchor: { flex: 1, justifyContent: "flex-end" },
168
+ });
@@ -0,0 +1,98 @@
1
+ import { useContext } from "react";
2
+ import { Pressable, Text, View } from "react-native";
3
+ import { cn } from "../cn";
4
+ import { MIN_TOUCH_TARGET, sizeStyle, textStyle, touchSlop, typeStyle } from "../controlStyles";
5
+ import { withAlpha } from "../palette";
6
+ import { useLargerText } from "../theme";
7
+ import { useScheme } from "../useScheme";
8
+ import { chatTextSize } from "./chat-text";
9
+ import { ChatTextColorContext } from "./response-text";
10
+
11
+ export type MessageQuoteProps = {
12
+ /** Who wrote the quoted message, such as "You" or the bot name. */
13
+ author: string;
14
+ /** The quoted text. Shown on up to two lines. */
15
+ text: string;
16
+ /** Makes the quote a button, so the app can scroll to and highlight the original. */
17
+ onPress?: () => void;
18
+ /** Names the button. Default "Go to quoted message from {author}". Used with `onPress` only. */
19
+ accessibilityLabel?: string;
20
+ className?: string;
21
+ testID?: string;
22
+ };
23
+
24
+ /** Lines of the quoted text before it is cut with an ellipsis. */
25
+ export const QUOTE_MAX_LINES = 2;
26
+
27
+ /**
28
+ * A compact quote inside a message bubble, above the message text: a left
29
+ * border, the author in the caption size and the quoted text on up to two
30
+ * lines. It takes its colour from the enclosing `MessageContent`, so it reads
31
+ * on the primary user bubble as well as on the tint and assistant ones. With
32
+ * `onPress` it is a button (44 point minimum height).
33
+ */
34
+ export function MessageQuote({
35
+ author,
36
+ text,
37
+ onPress,
38
+ accessibilityLabel,
39
+ className,
40
+ testID,
41
+ }: MessageQuoteProps) {
42
+ const scheme = useScheme();
43
+ const largerText = useLargerText();
44
+ const inherited = useContext(ChatTextColorContext);
45
+ const color = inherited ?? textStyle("foreground", scheme).color;
46
+ const caption = chatTextSize("caption", largerText);
47
+ const body = chatTextSize("small", largerText);
48
+ const content = (
49
+ <>
50
+ <Text
51
+ numberOfLines={1}
52
+ className={cn(caption, "font-instrument-sans-semibold")}
53
+ style={[sizeStyle(caption), typeStyle("semibold"), { color: withAlpha(color, 0.75) }]}
54
+ >
55
+ {author}
56
+ </Text>
57
+ <Text
58
+ numberOfLines={QUOTE_MAX_LINES}
59
+ className={cn(body, "font-instrument-sans")}
60
+ style={[sizeStyle(body), { color: withAlpha(color, 0.9) }]}
61
+ >
62
+ {text}
63
+ </Text>
64
+ </>
65
+ );
66
+ const style = {
67
+ borderLeftWidth: 3,
68
+ borderLeftColor: withAlpha(color, 0.45),
69
+ borderRadius: 4,
70
+ };
71
+ if (!onPress) {
72
+ return (
73
+ <View
74
+ testID={testID}
75
+ accessible
76
+ accessibilityLabel={`Quoted message from ${author}: ${text}`}
77
+ className={cn("min-w-0 gap-0.5 py-0.5 pl-2.5", className)}
78
+ style={style}
79
+ >
80
+ {content}
81
+ </View>
82
+ );
83
+ }
84
+ return (
85
+ <Pressable
86
+ testID={testID}
87
+ accessibilityRole="button"
88
+ accessibilityLabel={accessibilityLabel ?? `Go to quoted message from ${author}`}
89
+ accessibilityHint={text}
90
+ onPress={onPress}
91
+ hitSlop={touchSlop(MIN_TOUCH_TARGET)}
92
+ className={cn("min-h-11 min-w-0 justify-center gap-0.5 py-0.5 pl-2.5", className)}
93
+ style={style}
94
+ >
95
+ {content}
96
+ </Pressable>
97
+ );
98
+ }