@vvedantb/native-ui 0.13.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -21
- package/package.json +36 -3
- package/src/BottomSheetContainer.tsx +2 -2
- package/src/BulkActionBar.tsx +240 -105
- package/src/Button.tsx +11 -7
- package/src/ConfirmDialog.tsx +25 -35
- package/src/Dialog.tsx +47 -35
- package/src/DividedList.tsx +11 -7
- package/src/DropdownMenu.tsx +22 -8
- package/src/FloatingSurface.tsx +5 -2
- package/src/FloatingTabBar.tsx +13 -22
- package/src/HeaderIconButton.tsx +4 -3
- package/src/Input.tsx +2 -2
- package/src/LoadingState.tsx +132 -0
- package/src/PageTitle.tsx +9 -2
- package/src/Pager.tsx +62 -0
- package/src/PasswordInput.tsx +1 -1
- package/src/PillTabs.tsx +144 -34
- package/src/Progress.tsx +9 -7
- package/src/Select.tsx +4 -4
- package/src/Sheet.tsx +29 -22
- package/src/SheetRow.tsx +27 -9
- package/src/Skeleton.tsx +91 -9
- package/src/SurfaceRow.tsx +111 -0
- package/src/SwipeableRow.tsx +43 -0
- package/src/Switch.tsx +6 -4
- package/src/Tabs.tsx +68 -13
- package/src/Toast.tsx +107 -65
- package/src/ai/activity-step-detail.tsx +1 -3
- package/src/ai/activity-step-label.ts +981 -0
- package/src/ai/activity-step-types.ts +107 -0
- package/src/ai/activity-tasks-utils.ts +199 -0
- package/src/ai/activity-timeline.ts +45 -0
- package/src/ai/attachments.tsx +3 -1
- package/src/ai/chat-text.ts +13 -7
- package/src/ai/chat-thread.tsx +34 -27
- package/src/ai/choice-question.tsx +154 -35
- package/src/ai/code-block.tsx +189 -49
- package/src/ai/collapsible.tsx +23 -13
- package/src/ai/conversation.tsx +394 -57
- package/src/ai/icon-button.tsx +41 -6
- package/src/ai/index.ts +55 -4
- package/src/ai/message-action-sheet.tsx +4 -1
- package/src/ai/message.tsx +60 -31
- package/src/ai/prompt-input-menu.tsx +197 -0
- package/src/ai/prompt-input.tsx +301 -75
- package/src/ai/shimmer.tsx +4 -6
- package/src/ai/subagent-cards.tsx +84 -14
- package/src/ai/tool-approval-card.tsx +139 -8
- package/src/ai/trace.tsx +54 -17
- package/src/ai/unified-diff.tsx +182 -0
- package/src/auth/GoogleSignInButton.tsx +6 -3
- package/src/auth/SignInScreen.tsx +11 -2
- package/src/brand/ProviderIcon.tsx +136 -0
- package/src/brand/index.ts +2 -0
- package/src/configureTheme.ts +39 -11
- package/src/controlStyles.ts +46 -30
- package/src/entries/gesture-handler.ts +4 -0
- package/src/entries/keyboard-controller.ts +4 -0
- package/src/entries/markdown.ts +1 -1
- package/src/entries/primitives.ts +24 -3
- package/src/entries/theme.ts +2 -1
- package/src/entries/video.ts +4 -0
- package/src/entries/webview.ts +6 -0
- package/src/examples/AiChatGallery.tsx +70 -0
- package/src/floating.ts +25 -4
- package/src/haptics.ts +30 -4
- package/src/highlight.ts +302 -1
- package/src/index.ts +27 -5
- package/src/loadingGrid.ts +75 -0
- package/src/markdown/Markdown.tsx +234 -32
- package/src/markdown/MarkdownTable.tsx +188 -0
- package/src/markdown/tableText.ts +58 -0
- package/src/marketing/UpgradeToProCard.tsx +4 -3
- package/src/media/ImageGallery.tsx +224 -0
- package/src/media/MediaCard.tsx +100 -0
- package/src/media/imageGrid.ts +51 -0
- package/src/motion.ts +33 -0
- package/src/pagerSwipe.ts +102 -0
- package/src/palette.ts +8 -1
- package/src/primitives/button.tsx +4 -1
- package/src/primitives/keyboard-controller.tsx +106 -0
- package/src/primitives/keyboard-parts.tsx +104 -0
- package/src/primitives/overlay.tsx +133 -45
- package/src/primitives/pager-gesture.tsx +131 -0
- package/src/primitives/sheet-drag.tsx +305 -0
- package/src/primitives/sheet-gesture.tsx +181 -0
- package/src/primitives/swipe-row-gesture.tsx +290 -0
- package/src/primitives/toast-drag-gesture.tsx +92 -0
- package/src/primitives/web-click-guard.ts +50 -0
- package/src/relativeTime.ts +94 -0
- package/src/router/RouterStackPageHeader.tsx +2 -2
- package/src/settings/SettingsEmptyState.tsx +20 -10
- package/src/settings/SettingsField.tsx +11 -5
- package/src/settings/SettingsSection.tsx +8 -5
- package/src/settings/SettingsToggleRow.tsx +55 -22
- package/src/settings/relaxedSizeStyle.ts +11 -0
- package/src/sheetDrag.ts +67 -0
- package/src/swipeRow.ts +169 -0
- package/src/tabBarRoutes.ts +18 -0
- package/src/tabIndicator.ts +71 -0
- package/src/tailwind-preset.cjs +13 -6
- package/src/tailwind-preset.d.cts +5 -0
- package/src/theme.tsx +6 -2
- package/src/toastDrag.ts +58 -0
- package/src/typography.ts +20 -3
- package/src/useScheme.ts +10 -1
- package/src/video/InlineVideo.tsx +91 -0
- package/src/webview/HtmlView.tsx +91 -0
- package/src/webview/HtmlView.web.tsx +21 -0
- package/src/webview/htmlView.ts +45 -0
package/src/ai/icon-button.tsx
CHANGED
|
@@ -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),
|
|
23
|
-
*
|
|
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
|
|
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={
|
|
45
|
-
|
|
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={
|
|
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,
|
|
@@ -102,18 +107,48 @@ export type {
|
|
|
102
107
|
} from "./chain-of-thought";
|
|
103
108
|
export { ActivityStep } from "./activity-step";
|
|
104
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";
|
|
105
135
|
export {
|
|
106
136
|
PromptInput,
|
|
107
137
|
PromptInputButton,
|
|
138
|
+
PromptInputChip,
|
|
108
139
|
PromptInputSubmit,
|
|
140
|
+
PROMPT_INPUT_CHIP,
|
|
109
141
|
isGenerating,
|
|
110
142
|
} from "./prompt-input";
|
|
111
143
|
export type {
|
|
112
144
|
ChatStatus,
|
|
145
|
+
PromptInputChipProps,
|
|
113
146
|
PromptInputProps,
|
|
114
147
|
PromptInputButtonProps,
|
|
115
148
|
PromptInputSubmitProps,
|
|
116
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";
|
|
117
152
|
export { Suggestions, Suggestion } from "./suggestion";
|
|
118
153
|
export type { SuggestionsProps, SuggestionProps } from "./suggestion";
|
|
119
154
|
export { Sources, SourcesTrigger, SourcesContent, Source, sourcesLabel } from "./sources";
|
|
@@ -146,7 +181,23 @@ export { ChoiceQuestion, choiceAnswers } from "./choice-question";
|
|
|
146
181
|
export type { ChoiceQuestionProps, ChoiceQuestionItem } from "./choice-question";
|
|
147
182
|
export { ActivityStepDetail } from "./activity-step-detail";
|
|
148
183
|
export type { ActivityStepDetailProps, ActivityStepDetailStep } from "./activity-step-detail";
|
|
149
|
-
export {
|
|
150
|
-
export type {
|
|
184
|
+
export { UnifiedDiff, DiffStat, UNIFIED_DIFF } from "./unified-diff";
|
|
185
|
+
export type { UnifiedDiffProps, UnifiedDiffLine, DiffStatProps } from "./unified-diff";
|
|
186
|
+
export {
|
|
187
|
+
ToolApprovalCard,
|
|
188
|
+
ToolDraftCard,
|
|
189
|
+
summarizeArgs,
|
|
190
|
+
ARGS_MAX_CHARS,
|
|
191
|
+
changedDraftEdits,
|
|
192
|
+
DRAFT_ROLE_LABELS,
|
|
193
|
+
} from "./tool-approval-card";
|
|
194
|
+
export type {
|
|
195
|
+
ToolApprovalCardProps,
|
|
196
|
+
ToolDraftCardProps,
|
|
197
|
+
ToolDecision,
|
|
198
|
+
DraftField,
|
|
199
|
+
DraftFieldRole,
|
|
200
|
+
DraftEdit,
|
|
201
|
+
} from "./tool-approval-card";
|
|
151
202
|
export { SubagentRunCard, SubagentResultCard, subagentStatusColors } from "./subagent-cards";
|
|
152
|
-
export type { SubagentRunCardProps, SubagentResultCardProps, SubagentStatus } from "./subagent-cards";
|
|
203
|
+
export type { SubagentRunCardProps, SubagentResultCardProps, SubagentStatus, SubagentHandoff } from "./subagent-cards";
|
|
@@ -22,6 +22,9 @@ export type MessageSheetAction = {
|
|
|
22
22
|
onPress: () => void;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
/** The sheet's default title, also the message's `longpress` action label. */
|
|
26
|
+
export const MESSAGE_ACTIONS_TITLE = "Message actions";
|
|
27
|
+
|
|
25
28
|
export type MessageActionSheetProps = {
|
|
26
29
|
open: boolean;
|
|
27
30
|
/** Called on a backdrop tap, Cancel, Android back and after an action runs. */
|
|
@@ -45,7 +48,7 @@ export function MessageActionSheet({
|
|
|
45
48
|
open,
|
|
46
49
|
onClose,
|
|
47
50
|
actions,
|
|
48
|
-
title =
|
|
51
|
+
title = MESSAGE_ACTIONS_TITLE,
|
|
49
52
|
cancelLabel = "Cancel",
|
|
50
53
|
testID = "message-action-sheet",
|
|
51
54
|
}: MessageActionSheetProps) {
|
package/src/ai/message.tsx
CHANGED
|
@@ -16,9 +16,9 @@ import { useLargerText } from "../theme";
|
|
|
16
16
|
import { tokens } from "../tokens";
|
|
17
17
|
import { useReduceMotion } from "../useReduceMotion";
|
|
18
18
|
import { useScheme } from "../useScheme";
|
|
19
|
-
import { chatTextSize } from "./chat-text";
|
|
20
|
-
import { ChatIconButton } from "./icon-button";
|
|
21
|
-
import { MessageActionSheet, type MessageSheetAction } from "./message-action-sheet";
|
|
19
|
+
import { META_TEXT_STYLE, chatTextSize } from "./chat-text";
|
|
20
|
+
import { ChatIconButton, type ChatIconButtonSize } from "./icon-button";
|
|
21
|
+
import { MESSAGE_ACTIONS_TITLE, MessageActionSheet, type MessageSheetAction } from "./message-action-sheet";
|
|
22
22
|
import { ChatTextColorContext, ResponseText } from "./response-text";
|
|
23
23
|
|
|
24
24
|
export type MessageRole = "user" | "assistant" | "system";
|
|
@@ -68,13 +68,13 @@ function MessageHighlight() {
|
|
|
68
68
|
const wash = useSharedValue(reduceMotion ? 0.5 : 1);
|
|
69
69
|
useEffect(() => {
|
|
70
70
|
if (reduceMotion) {
|
|
71
|
-
wash.
|
|
71
|
+
wash.set(0.5);
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
|
-
wash.
|
|
75
|
-
wash.
|
|
74
|
+
wash.set(1);
|
|
75
|
+
wash.set(withTiming(0, { duration: MESSAGE_HIGHLIGHT_MS }));
|
|
76
76
|
}, [wash, reduceMotion]);
|
|
77
|
-
const animatedStyle = useAnimatedStyle(() => ({ opacity: wash.
|
|
77
|
+
const animatedStyle = useAnimatedStyle(() => ({ opacity: wash.get() }));
|
|
78
78
|
return (
|
|
79
79
|
<Animated.View
|
|
80
80
|
testID="message-highlight"
|
|
@@ -100,9 +100,9 @@ function MessageHighlight() {
|
|
|
100
100
|
*
|
|
101
101
|
* With `onLongPress` or `actions` the message is a `Pressable` (same layout,
|
|
102
102
|
* `className`, `style` and view props) and a long press opens the
|
|
103
|
-
* `MessageActionSheet
|
|
104
|
-
*
|
|
105
|
-
* for screen reader users. `highlighted` draws the quote-jump highlight.
|
|
103
|
+
* `MessageActionSheet`; TalkBack reaches it as the `longpress` action. It stays
|
|
104
|
+
* a non-accessible container so its buttons and links keep their own focus:
|
|
105
|
+
* also offer the actions in a `MessageActionsBar` for screen reader users. `highlighted` draws the quote-jump highlight.
|
|
106
106
|
*/
|
|
107
107
|
export function Message({
|
|
108
108
|
from,
|
|
@@ -120,10 +120,14 @@ export function Message({
|
|
|
120
120
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
121
121
|
const hasActions = actions != null && actions.length > 0;
|
|
122
122
|
const longPressable = onLongPress != null || hasActions;
|
|
123
|
-
const
|
|
123
|
+
const longPress = () => {
|
|
124
|
+
onLongPress?.();
|
|
125
|
+
if (hasActions) setSheetOpen(true);
|
|
126
|
+
};
|
|
127
|
+
const layout: ViewProps["style"] = [
|
|
124
128
|
{ alignSelf: user ? "flex-end" : "flex-start", alignItems: user ? "flex-end" : "stretch" },
|
|
125
129
|
style,
|
|
126
|
-
]
|
|
130
|
+
];
|
|
127
131
|
const merged = cn("w-full max-w-[95%] gap-2", className);
|
|
128
132
|
const parts = (
|
|
129
133
|
<>
|
|
@@ -139,10 +143,12 @@ export function Message({
|
|
|
139
143
|
<Pressable
|
|
140
144
|
accessible={false}
|
|
141
145
|
className={merged}
|
|
142
|
-
style={layout
|
|
143
|
-
onLongPress={
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
style={layout}
|
|
147
|
+
onLongPress={longPress}
|
|
148
|
+
// TalkBack's actions menu reaches the same sheet as a long press.
|
|
149
|
+
accessibilityActions={[{ name: "longpress", label: actionSheetTitle ?? MESSAGE_ACTIONS_TITLE }]}
|
|
150
|
+
onAccessibilityAction={(event) => {
|
|
151
|
+
if (event.nativeEvent.actionName === "longpress") longPress();
|
|
146
152
|
}}
|
|
147
153
|
{...props}
|
|
148
154
|
>
|
|
@@ -181,8 +187,8 @@ export function userBubbleStyle(scheme: "light" | "dark") {
|
|
|
181
187
|
/**
|
|
182
188
|
* A user bubble's look. `primary` is the web default (`bg-primary` with the
|
|
183
189
|
* label colour). `tint` is the chat override Eva uses (`rounded-surface
|
|
184
|
-
* bg-primary/10 px-
|
|
185
|
-
* with foreground text and every corner at 16.
|
|
190
|
+
* bg-primary/10 text-foreground`, still `px-4 py-3`): a 10% wash of the
|
|
191
|
+
* primary colour with foreground text and every corner at 16.
|
|
186
192
|
*/
|
|
187
193
|
export type MessageBubble = "primary" | "tint";
|
|
188
194
|
|
|
@@ -239,9 +245,9 @@ export type MessageContentProps = ViewProps & {
|
|
|
239
245
|
};
|
|
240
246
|
|
|
241
247
|
/**
|
|
242
|
-
* The message body. A user message is a filled bubble
|
|
243
|
-
* with the primary fill and label colour, or with `bubble="tint"`
|
|
244
|
-
*
|
|
248
|
+
* The message body. A user message is a filled bubble, `px-4 py-3`: by
|
|
249
|
+
* default with the primary fill and label colour, or with `bubble="tint"` on
|
|
250
|
+
* a 10% primary wash with the foreground colour. An assistant message has
|
|
245
251
|
* no fill and the foreground colour. A string child is drawn as
|
|
246
252
|
* `ResponseText`, and `ResponseText` inside takes the colour.
|
|
247
253
|
*/
|
|
@@ -256,7 +262,7 @@ export function MessageContent({ children, bubble = "primary", quote, className,
|
|
|
256
262
|
return (
|
|
257
263
|
<ChatTextColorContext.Provider value={color}>
|
|
258
264
|
<View
|
|
259
|
-
className={cn("min-w-0 max-w-full gap-2", user &&
|
|
265
|
+
className={cn("min-w-0 max-w-full gap-2", user && "px-4 py-3", className)}
|
|
260
266
|
style={[bubbleStyle, style]}
|
|
261
267
|
{...props}
|
|
262
268
|
>
|
|
@@ -273,21 +279,33 @@ export function MessageContent({ children, bubble = "primary", quote, className,
|
|
|
273
279
|
);
|
|
274
280
|
}
|
|
275
281
|
|
|
276
|
-
export type MessageMetaProps = {
|
|
282
|
+
export type MessageMetaProps = {
|
|
283
|
+
children: ReactNode;
|
|
284
|
+
/**
|
|
285
|
+
* Default `caption` (`text-xs`, 12 / 16). `2xs` is the web `text-2xs` token
|
|
286
|
+
* (11 / 16), as Eva's web chat. Larger text draws both at the caption step.
|
|
287
|
+
*/
|
|
288
|
+
size?: "caption" | "2xs";
|
|
289
|
+
className?: string;
|
|
290
|
+
};
|
|
277
291
|
|
|
278
292
|
/**
|
|
279
293
|
* The muted clock and duration after a message's actions, as Eva's web chat
|
|
280
|
-
* (`
|
|
281
|
-
*
|
|
294
|
+
* (`tabular-nums text-muted-foreground/60`, "10:21 PM · 1m"): the caption
|
|
295
|
+
* size (or `size="2xs"`), the muted colour at 60%, tabular figures.
|
|
282
296
|
*/
|
|
283
|
-
export function MessageMeta({ children, className }: MessageMetaProps) {
|
|
297
|
+
export function MessageMeta({ children, size = "caption", className }: MessageMetaProps) {
|
|
284
298
|
const scheme = useScheme();
|
|
285
299
|
const largerText = useLargerText();
|
|
286
|
-
const
|
|
300
|
+
const caption = chatTextSize("caption", largerText);
|
|
301
|
+
const tiny = size === "2xs" && !largerText;
|
|
287
302
|
return (
|
|
288
303
|
<Text
|
|
289
|
-
className={cn(
|
|
290
|
-
style={[
|
|
304
|
+
className={cn(!tiny && caption, "font-instrument-sans", className)}
|
|
305
|
+
style={[
|
|
306
|
+
tiny ? META_TEXT_STYLE : sizeStyle(caption),
|
|
307
|
+
{ color: withAlpha(tokens.color.muted[scheme], 0.6), fontVariant: ["tabular-nums"] },
|
|
308
|
+
]}
|
|
291
309
|
>
|
|
292
310
|
{children}
|
|
293
311
|
</Text>
|
|
@@ -325,12 +343,20 @@ export type MessageActionProps = {
|
|
|
325
343
|
*/
|
|
326
344
|
selected?: boolean;
|
|
327
345
|
disabled?: boolean;
|
|
346
|
+
/**
|
|
347
|
+
* Default `sm`, the web `icon-sm` (32, 16 icon). `xs` is the web `icon-xs`
|
|
348
|
+
* (28, 14 icon), as Eva's web chat; `touch` is a whole 44 square (16 icon)
|
|
349
|
+
* for a phone, so the row needs no slop.
|
|
350
|
+
*/
|
|
351
|
+
size?: ChatIconButtonSize;
|
|
328
352
|
testID?: string;
|
|
329
353
|
};
|
|
330
354
|
|
|
331
355
|
/**
|
|
332
|
-
* A ghost icon button under a message: the web `icon-sm` (32, 16 icon)
|
|
333
|
-
*
|
|
356
|
+
* A ghost icon button under a message: the web `icon-sm` (32, 16 icon), or
|
|
357
|
+
* `icon-xs` with `size="xs"`, with the slop to 44 high. It has no sideways
|
|
358
|
+
* slop: the actions sit 2 to 4 apart, and slop there would overlap the next
|
|
359
|
+
* one, so a tap on Copy could fork or rate instead. Copying is up to the app (`onPress`): React Native's own
|
|
334
360
|
* `Clipboard` is deprecated, and native-ui adds no clipboard package.
|
|
335
361
|
*/
|
|
336
362
|
export function MessageAction({
|
|
@@ -340,6 +366,7 @@ export function MessageAction({
|
|
|
340
366
|
onPress,
|
|
341
367
|
selected = false,
|
|
342
368
|
disabled,
|
|
369
|
+
size,
|
|
343
370
|
testID,
|
|
344
371
|
}: MessageActionProps) {
|
|
345
372
|
const scheme = useScheme();
|
|
@@ -353,6 +380,8 @@ export function MessageAction({
|
|
|
353
380
|
accessibilityState={{ selected }}
|
|
354
381
|
onPress={onPress}
|
|
355
382
|
disabled={disabled}
|
|
383
|
+
size={size}
|
|
384
|
+
slop="vertical"
|
|
356
385
|
testID={testID}
|
|
357
386
|
/>
|
|
358
387
|
);
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Children, useEffect, type ReactNode } from "react";
|
|
2
|
+
import { BackHandler, Pressable, ScrollView, Text, View, useWindowDimensions } from "react-native";
|
|
3
|
+
import { cn } from "../cn";
|
|
4
|
+
import { MIN_TOUCH_TARGET, sizeStyle, textStyle, touchSlop, typeStyle } from "../controlStyles";
|
|
5
|
+
import { palette, withAlpha } from "../palette";
|
|
6
|
+
import { useLargerText } from "../theme";
|
|
7
|
+
import { tokens } from "../tokens";
|
|
8
|
+
import { useScheme } from "../useScheme";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The web panel picker height (`computePanelPopupPlacement`): at most 340 and
|
|
12
|
+
* at least 160, here a share of the window, as a phone has the keyboard up.
|
|
13
|
+
*/
|
|
14
|
+
export const PROMPT_INPUT_MENU = { maxHeight: 340, minHeight: 160, windowShare: 0.35 } as const;
|
|
15
|
+
|
|
16
|
+
/** Menu height for a window height: the share, kept inside the web's 160 to 340. */
|
|
17
|
+
export function promptInputMenuHeight(windowHeight: number): number {
|
|
18
|
+
return Math.round(
|
|
19
|
+
Math.max(PROMPT_INPUT_MENU.minHeight, Math.min(PROMPT_INPUT_MENU.maxHeight, windowHeight * PROMPT_INPUT_MENU.windowShare)),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type PromptInputMenuProps = {
|
|
24
|
+
/** Names the list for VoiceOver and TalkBack, as the web listbox `aria-label` (such as "Mentions" or "Skills"). */
|
|
25
|
+
title: string;
|
|
26
|
+
/** `PromptInputMenuItem`s. With none, `empty` shows instead. */
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
/** Shown when there are no rows. Default "No matches.", as the web. */
|
|
29
|
+
empty?: ReactNode;
|
|
30
|
+
/**
|
|
31
|
+
* Closes the picker, as Escape does on the web. Android back calls it while
|
|
32
|
+
* the picker is mounted, before the screen behind gets the press.
|
|
33
|
+
*/
|
|
34
|
+
onDismiss?: () => void;
|
|
35
|
+
testID?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The web `@` / `/` picker (`MentionPickerPopup`, `panel` layout) for touch:
|
|
40
|
+
* the composer's full width, docked right above the field, so it rides the
|
|
41
|
+
* keyboard with it. Give it to `PromptInput` as `menu`. The popover surface
|
|
42
|
+
* (`rounded-surface bg-popover`, the field border in place of the web shadow
|
|
43
|
+
* ring, as the AI parts draw no shadow) holds a `py-1` list that scrolls past
|
|
44
|
+
* `promptInputMenuHeight`. Taps reach a row while the keyboard
|
|
45
|
+
* is up, so picking never drops the keyboard.
|
|
46
|
+
*/
|
|
47
|
+
export function PromptInputMenu({ title, children, empty = "No matches.", onDismiss, testID }: PromptInputMenuProps) {
|
|
48
|
+
const scheme = useScheme();
|
|
49
|
+
// Back closes the picker first. An effect: the subscription is a native one.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!onDismiss) return;
|
|
52
|
+
const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
|
|
53
|
+
onDismiss();
|
|
54
|
+
return true;
|
|
55
|
+
});
|
|
56
|
+
return () => subscription.remove();
|
|
57
|
+
}, [onDismiss]);
|
|
58
|
+
const { height } = useWindowDimensions();
|
|
59
|
+
const hasRows = Children.toArray(children).length > 0;
|
|
60
|
+
return (
|
|
61
|
+
<View
|
|
62
|
+
testID={testID}
|
|
63
|
+
className="overflow-hidden border"
|
|
64
|
+
style={{
|
|
65
|
+
backgroundColor: palette.overlay[scheme],
|
|
66
|
+
borderColor: tokens.color.border[scheme],
|
|
67
|
+
borderRadius: tokens.radius.lg,
|
|
68
|
+
maxHeight: promptInputMenuHeight(height),
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{hasRows ? (
|
|
72
|
+
<ScrollView
|
|
73
|
+
accessibilityRole="list"
|
|
74
|
+
accessibilityLabel={title}
|
|
75
|
+
keyboardShouldPersistTaps="always"
|
|
76
|
+
contentContainerClassName="py-1"
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</ScrollView>
|
|
80
|
+
) : (
|
|
81
|
+
// Web: `px-3 py-2.5 text-xs leading-relaxed text-muted-foreground`.
|
|
82
|
+
<Text
|
|
83
|
+
className="px-3 py-2.5 font-instrument-sans text-xs"
|
|
84
|
+
style={[sizeStyle("text-xs", "relaxed"), textStyle("muted", scheme)]}
|
|
85
|
+
>
|
|
86
|
+
{empty}
|
|
87
|
+
</Text>
|
|
88
|
+
)}
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Web badge chip: `rounded-md bg-muted px-1.5 py-0.5 text-3xs font-medium` (10). */
|
|
94
|
+
const BADGE_TEXT = { fontSize: 10, lineHeight: 12 } as const;
|
|
95
|
+
|
|
96
|
+
export type PromptInputMenuItemProps = {
|
|
97
|
+
/** The character that inserts this row, muted before the label: `@` or `/`. Left out when `leading` names a person. */
|
|
98
|
+
prefix?: string;
|
|
99
|
+
label: string;
|
|
100
|
+
/** One muted line under the label, such as a skill's description. */
|
|
101
|
+
description?: string;
|
|
102
|
+
/** Before the text, such as a teammate's `Avatar` (24). */
|
|
103
|
+
leading?: ReactNode;
|
|
104
|
+
/** At the end in place of `badge`, such as a 16 kind icon. */
|
|
105
|
+
trailing?: ReactNode;
|
|
106
|
+
/** A small source chip at the end (such as "Eva" or "Claude"), with an optional 11 `badgeIcon`. */
|
|
107
|
+
badge?: string;
|
|
108
|
+
badgeIcon?: ReactNode;
|
|
109
|
+
/** The row Return picks, tinted as the web's keyboard-selected row. */
|
|
110
|
+
selected?: boolean;
|
|
111
|
+
onPress?: () => void;
|
|
112
|
+
testID?: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** A description longer than this is cut with "…", as the web `previewOneLine`. */
|
|
116
|
+
const DESCRIPTION_MAX = 72;
|
|
117
|
+
|
|
118
|
+
function oneLine(text: string): string {
|
|
119
|
+
const line = text.replace(/\s+/g, " ").trim();
|
|
120
|
+
return line.length <= DESCRIPTION_MAX ? line : `${line.slice(0, DESCRIPTION_MAX - 1)}…`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* One picker row, the web `MentionRow` in its option button
|
|
125
|
+
* (`mx-1 rounded-lg px-2 text-sm`), at least 44 high. `selected` takes the
|
|
126
|
+
* web `bg-primary/15` (`/25` dark) and a medium label; a press fades to 70%,
|
|
127
|
+
* as the composer chips, in place of the web hover fill.
|
|
128
|
+
*/
|
|
129
|
+
export function PromptInputMenuItem({
|
|
130
|
+
prefix,
|
|
131
|
+
label,
|
|
132
|
+
description,
|
|
133
|
+
leading,
|
|
134
|
+
trailing,
|
|
135
|
+
badge,
|
|
136
|
+
badgeIcon,
|
|
137
|
+
selected = false,
|
|
138
|
+
onPress,
|
|
139
|
+
testID,
|
|
140
|
+
}: PromptInputMenuItemProps) {
|
|
141
|
+
const scheme = useScheme();
|
|
142
|
+
const largerText = useLargerText();
|
|
143
|
+
const muted = textStyle("muted", scheme);
|
|
144
|
+
const labelSize = sizeStyle(largerText ? "text-lg" : "text-sm");
|
|
145
|
+
const detail = description ? oneLine(description) : undefined;
|
|
146
|
+
const end =
|
|
147
|
+
trailing ??
|
|
148
|
+
(badge ? (
|
|
149
|
+
<View
|
|
150
|
+
className="shrink-0 flex-row items-center gap-1 px-1.5 py-0.5"
|
|
151
|
+
style={{ backgroundColor: tokens.color.surfaceSecondary[scheme], borderRadius: 6 }}
|
|
152
|
+
>
|
|
153
|
+
{badgeIcon}
|
|
154
|
+
<Text className="font-instrument-sans-medium" style={[BADGE_TEXT, muted, typeStyle("medium")]}>
|
|
155
|
+
{badge}
|
|
156
|
+
</Text>
|
|
157
|
+
</View>
|
|
158
|
+
) : null);
|
|
159
|
+
return (
|
|
160
|
+
<Pressable
|
|
161
|
+
testID={testID}
|
|
162
|
+
accessibilityRole="button"
|
|
163
|
+
accessibilityState={{ selected }}
|
|
164
|
+
accessibilityLabel={[`${prefix ?? ""}${label}`, detail, badge].filter(Boolean).join(", ")}
|
|
165
|
+
onPress={onPress}
|
|
166
|
+
// Already 44 high and the list's width: no slop past the row.
|
|
167
|
+
hitSlop={touchSlop(MIN_TOUCH_TARGET)}
|
|
168
|
+
// A plain style: NativeWind drops a function `style` beside `className`.
|
|
169
|
+
className={cn("mx-1 flex-row items-center gap-2 px-2 py-1.5 active:opacity-70", largerText && "py-2")}
|
|
170
|
+
style={{
|
|
171
|
+
minHeight: MIN_TOUCH_TARGET,
|
|
172
|
+
borderRadius: 8,
|
|
173
|
+
backgroundColor: selected ? withAlpha(palette.primary[scheme], scheme === "dark" ? 0.25 : 0.15) : "transparent",
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
{leading}
|
|
177
|
+
<View className="min-w-0 flex-1 gap-0.5">
|
|
178
|
+
<View className="min-w-0 flex-row items-center gap-1.5">
|
|
179
|
+
<Text
|
|
180
|
+
numberOfLines={1}
|
|
181
|
+
className={cn("min-w-0 flex-1", selected ? "font-instrument-sans-medium" : "font-instrument-sans")}
|
|
182
|
+
style={[labelSize, textStyle("foreground", scheme), selected ? typeStyle("medium") : null]}
|
|
183
|
+
>
|
|
184
|
+
{prefix ? <Text style={muted}>{prefix}</Text> : null}
|
|
185
|
+
{label}
|
|
186
|
+
</Text>
|
|
187
|
+
{end}
|
|
188
|
+
</View>
|
|
189
|
+
{detail ? (
|
|
190
|
+
<Text numberOfLines={1} className="font-instrument-sans text-xs" style={[sizeStyle("text-xs"), muted]}>
|
|
191
|
+
{detail}
|
|
192
|
+
</Text>
|
|
193
|
+
) : null}
|
|
194
|
+
</View>
|
|
195
|
+
</Pressable>
|
|
196
|
+
);
|
|
197
|
+
}
|