@opengeni/react-native 1.4.5-canary.37615713321001
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/LICENSE +190 -0
- package/README.md +74 -0
- package/dist/adapters.d.ts +53 -0
- package/dist/adapters.js +3 -0
- package/dist/adapters.js.map +1 -0
- package/dist/attachments.d.ts +75 -0
- package/dist/chunk-3VSZAQ7G.js +391 -0
- package/dist/chunk-3VSZAQ7G.js.map +1 -0
- package/dist/chunk-5ZVK7WKA.js +88 -0
- package/dist/chunk-5ZVK7WKA.js.map +1 -0
- package/dist/chunk-EEDIMY3L.js +240 -0
- package/dist/chunk-EEDIMY3L.js.map +1 -0
- package/dist/chunk-M23SDI4R.js +83 -0
- package/dist/chunk-M23SDI4R.js.map +1 -0
- package/dist/chunk-PHJBSAR3.js +2006 -0
- package/dist/chunk-PHJBSAR3.js.map +1 -0
- package/dist/environment.d.ts +19 -0
- package/dist/expo.d.ts +25 -0
- package/dist/expo.js +267 -0
- package/dist/expo.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +3345 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation.d.ts +262 -0
- package/dist/read-acknowledgement.d.ts +39 -0
- package/dist/realtime/call-events.d.ts +11 -0
- package/dist/realtime/call-view.d.ts +26 -0
- package/dist/realtime/expo-call.d.ts +11 -0
- package/dist/realtime/native-call.d.ts +104 -0
- package/dist/realtime/native-realtime.d.ts +35 -0
- package/dist/realtime/use-native-session-realtime.d.ts +25 -0
- package/dist/session-view.d.ts +87 -0
- package/dist/timeline/activity.d.ts +34 -0
- package/dist/timeline/composer.d.ts +86 -0
- package/dist/timeline/controls.d.ts +27 -0
- package/dist/timeline/decisions.d.ts +29 -0
- package/dist/timeline/feedback.d.ts +26 -0
- package/dist/timeline/icon-nodes.generated.d.ts +738 -0
- package/dist/timeline/icon.d.ts +11 -0
- package/dist/timeline/index.d.ts +32 -0
- package/dist/timeline/index.js +5419 -0
- package/dist/timeline/index.js.map +1 -0
- package/dist/timeline/markdown.d.ts +11 -0
- package/dist/timeline/markdown.js +415 -0
- package/dist/timeline/markdown.js.map +1 -0
- package/dist/timeline/message-attachments.d.ts +13 -0
- package/dist/timeline/message-timeline.d.ts +36 -0
- package/dist/timeline/model-picker.d.ts +17 -0
- package/dist/timeline/primitives.d.ts +85 -0
- package/dist/timeline/queue-dock.d.ts +19 -0
- package/dist/timeline/session-actions.d.ts +34 -0
- package/dist/timeline/session-list.d.ts +50 -0
- package/dist/timeline/session-screen.d.ts +35 -0
- package/dist/timeline/session-signals.d.ts +37 -0
- package/dist/timeline/sheet.d.ts +14 -0
- package/dist/timeline/theme.d.ts +58 -0
- package/dist/timeline/thinking-orb.d.ts +13 -0
- package/dist/timeline/turn.d.ts +80 -0
- package/dist/ui/attention.d.ts +14 -0
- package/dist/ui/cards.d.ts +21 -0
- package/dist/ui/composer.d.ts +21 -0
- package/dist/ui/format.d.ts +13 -0
- package/dist/ui/icons.d.ts +9 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +1757 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/steps-sheet.d.ts +10 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/timeline.d.ts +48 -0
- package/dist/ui/web-tokens.gen.d.ts +90 -0
- package/dist/use-native-session.d.ts +40 -0
- package/dist/view-model.d.ts +52 -0
- package/dist/voice-input.d.ts +102 -0
- package/dist/webrtc.d.ts +9 -0
- package/dist/webrtc.js +19 -0
- package/dist/webrtc.js.map +1 -0
- package/expo-module.config.json +7 -0
- package/ios/OpenGeniCall.podspec +22 -0
- package/ios/OpenGeniCallAppDelegateSubscriber.swift +29 -0
- package/ios/OpenGeniCallCenter.swift +189 -0
- package/ios/OpenGeniCallModule.swift +64 -0
- package/package.json +93 -0
- package/src/adapters.ts +150 -0
- package/src/attachments.ts +396 -0
- package/src/environment.tsx +214 -0
- package/src/expo.ts +295 -0
- package/src/index.ts +98 -0
- package/src/presentation.ts +285 -0
- package/src/read-acknowledgement.ts +72 -0
- package/src/realtime/call-events.ts +41 -0
- package/src/realtime/call-view.tsx +220 -0
- package/src/realtime/expo-call.ts +49 -0
- package/src/realtime/native-call.ts +266 -0
- package/src/realtime/native-realtime.ts +115 -0
- package/src/realtime/use-native-session-realtime.ts +29 -0
- package/src/session-view.tsx +2251 -0
- package/src/timeline/activity.tsx +736 -0
- package/src/timeline/composer.tsx +686 -0
- package/src/timeline/controls.tsx +151 -0
- package/src/timeline/decisions.tsx +651 -0
- package/src/timeline/feedback.tsx +251 -0
- package/src/timeline/icon-nodes.generated.ts +1830 -0
- package/src/timeline/icon.tsx +44 -0
- package/src/timeline/index.ts +67 -0
- package/src/timeline/markdown.tsx +510 -0
- package/src/timeline/message-attachments.tsx +155 -0
- package/src/timeline/message-timeline.tsx +1042 -0
- package/src/timeline/model-picker.tsx +450 -0
- package/src/timeline/primitives.tsx +561 -0
- package/src/timeline/queue-dock.tsx +331 -0
- package/src/timeline/session-actions.tsx +379 -0
- package/src/timeline/session-list.tsx +338 -0
- package/src/timeline/session-screen.tsx +488 -0
- package/src/timeline/session-signals.tsx +312 -0
- package/src/timeline/sheet.tsx +153 -0
- package/src/timeline/theme.tsx +128 -0
- package/src/timeline/thinking-orb.tsx +93 -0
- package/src/timeline/turn.tsx +569 -0
- package/src/ui/attention.tsx +220 -0
- package/src/ui/cards.tsx +267 -0
- package/src/ui/composer.tsx +296 -0
- package/src/ui/format.ts +86 -0
- package/src/ui/icons.tsx +131 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/steps-sheet.tsx +71 -0
- package/src/ui/theme.ts +261 -0
- package/src/ui/timeline.tsx +837 -0
- package/src/ui/web-tokens.gen.ts +94 -0
- package/src/use-native-session.ts +219 -0
- package/src/view-model.ts +485 -0
- package/src/voice-input.ts +382 -0
- package/src/webrtc.ts +30 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { stringifyPayload } from "@opengeni/react/timeline-model";
|
|
2
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
|
3
|
+
import { Pressable, ScrollView, Text, View, type TextStyle } from "react-native";
|
|
4
|
+
import Animated, {
|
|
5
|
+
cancelAnimation,
|
|
6
|
+
Easing,
|
|
7
|
+
interpolateColor,
|
|
8
|
+
useAnimatedStyle,
|
|
9
|
+
useSharedValue,
|
|
10
|
+
withRepeat,
|
|
11
|
+
withTiming,
|
|
12
|
+
} from "react-native-reanimated";
|
|
13
|
+
import { Icon, type NativeIconName } from "./icon";
|
|
14
|
+
import { fontStyle, useNativeTimelineTheme, type NativeTimelineTheme } from "./theme";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A reader opening or closing a disclosure owns the scroll position: the
|
|
18
|
+
* timeline stops following the tip (web: an aria-expanded click releases pin).
|
|
19
|
+
*/
|
|
20
|
+
const ReleaseFollowContext = createContext<() => void>(() => undefined);
|
|
21
|
+
export const ReleaseFollowProvider = ReleaseFollowContext.Provider;
|
|
22
|
+
export function useReleaseFollow(): () => void {
|
|
23
|
+
return useContext(ReleaseFollowContext);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* ----------------------------------------------------------------------------
|
|
27
|
+
Native counterparts of the web timeline primitives (timeline/shared.tsx).
|
|
28
|
+
Same anatomy and tokens: chevron, tinted icon, title, quiet preview, at most
|
|
29
|
+
one right-gutter chip; the body mounts only when expanded.
|
|
30
|
+
-------------------------------------------------------------------------- */
|
|
31
|
+
|
|
32
|
+
export type DisclosureChip = { tone: "ok" | "bad" | "muted" | "interrupted"; text: string };
|
|
33
|
+
export type IconTone = "accent" | "failed" | "running" | "muted";
|
|
34
|
+
|
|
35
|
+
export function iconToneColor(theme: NativeTimelineTheme, tone: IconTone): string {
|
|
36
|
+
switch (tone) {
|
|
37
|
+
case "accent":
|
|
38
|
+
return theme.colors.accent;
|
|
39
|
+
case "failed":
|
|
40
|
+
return theme.colors["status-failed"];
|
|
41
|
+
case "running":
|
|
42
|
+
return theme.colors["status-running"];
|
|
43
|
+
default:
|
|
44
|
+
return theme.colors["fg-subtle"];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Web `pointer-coarse:min-h-11`: every disclosure row is a 44pt touch target. */
|
|
49
|
+
export const ROW_MIN_HEIGHT = 44;
|
|
50
|
+
|
|
51
|
+
export function Chip({ chip }: { chip: DisclosureChip }) {
|
|
52
|
+
const theme = useNativeTimelineTheme();
|
|
53
|
+
const text: TextStyle = {
|
|
54
|
+
...fontStyle(theme, 400, "mono"),
|
|
55
|
+
fontSize: theme.size.xs,
|
|
56
|
+
lineHeight: theme.size.xs + 2,
|
|
57
|
+
color: chip.tone === "bad" ? theme.colors["status-failed"] : theme.colors["fg-subtle"],
|
|
58
|
+
};
|
|
59
|
+
if (chip.tone === "bad") {
|
|
60
|
+
return (
|
|
61
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: chip.text ? 6 : 0 }}>
|
|
62
|
+
<View
|
|
63
|
+
style={{
|
|
64
|
+
width: 6,
|
|
65
|
+
height: 6,
|
|
66
|
+
borderRadius: 3,
|
|
67
|
+
backgroundColor: theme.colors["status-failed"],
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
{chip.text ? <Text style={text}>{chip.text}</Text> : null}
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return <Text style={text}>{chip.text}</Text>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The web `animate-og-pulse` dot that rides a running preview. */
|
|
78
|
+
export function PulseDot({ color, size = 6 }: { color: string; size?: number }) {
|
|
79
|
+
const value = useSharedValue(1);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
value.value = withRepeat(
|
|
82
|
+
withTiming(0.35, { duration: 900, easing: Easing.inOut(Easing.ease) }),
|
|
83
|
+
-1,
|
|
84
|
+
true,
|
|
85
|
+
);
|
|
86
|
+
return () => cancelAnimation(value);
|
|
87
|
+
}, [value]);
|
|
88
|
+
const style = useAnimatedStyle(() => ({ opacity: value.value }));
|
|
89
|
+
return (
|
|
90
|
+
<Animated.View
|
|
91
|
+
style={[{ width: size, height: size, borderRadius: size / 2, backgroundColor: color }, style]}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The web `og-shimmer-text` (a moving fg-subtle → fg → fg-subtle gradient) as an
|
|
98
|
+
* ink sweep between the same two tokens.
|
|
99
|
+
*/
|
|
100
|
+
export function ShimmerText({
|
|
101
|
+
children,
|
|
102
|
+
style,
|
|
103
|
+
active = true,
|
|
104
|
+
numberOfLines,
|
|
105
|
+
}: {
|
|
106
|
+
children: ReactNode;
|
|
107
|
+
style?: TextStyle | TextStyle[];
|
|
108
|
+
active?: boolean;
|
|
109
|
+
numberOfLines?: number;
|
|
110
|
+
}) {
|
|
111
|
+
const theme = useNativeTimelineTheme();
|
|
112
|
+
const value = useSharedValue(0);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!active) return;
|
|
115
|
+
value.value = withRepeat(
|
|
116
|
+
withTiming(1, { duration: 1400, easing: Easing.inOut(Easing.ease) }),
|
|
117
|
+
-1,
|
|
118
|
+
true,
|
|
119
|
+
);
|
|
120
|
+
return () => cancelAnimation(value);
|
|
121
|
+
}, [active, value]);
|
|
122
|
+
const subtle = theme.colors["fg-subtle"];
|
|
123
|
+
const strong = theme.colors.fg;
|
|
124
|
+
const animated = useAnimatedStyle(() => ({
|
|
125
|
+
color: interpolateColor(value.value, [0, 1], [subtle, strong]),
|
|
126
|
+
}));
|
|
127
|
+
if (!active) {
|
|
128
|
+
return (
|
|
129
|
+
<Text style={style} numberOfLines={numberOfLines}>
|
|
130
|
+
{children}
|
|
131
|
+
</Text>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return (
|
|
135
|
+
<Animated.Text style={[style, animated]} numberOfLines={numberOfLines}>
|
|
136
|
+
{children}
|
|
137
|
+
</Animated.Text>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ActivityDisclosureProps {
|
|
142
|
+
icon: NativeIconName | ReactNode;
|
|
143
|
+
iconTone?: IconTone | undefined;
|
|
144
|
+
title: ReactNode;
|
|
145
|
+
titleMono?: boolean | undefined;
|
|
146
|
+
/** Title shimmer + preview pulse while in flight. */
|
|
147
|
+
running?: boolean | undefined;
|
|
148
|
+
preview?: ReactNode | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* A small trailing element (a thumbnail, an open action) shown in the right
|
|
151
|
+
* gutter in place of the preview and chip while collapsed, as on web.
|
|
152
|
+
*/
|
|
153
|
+
media?: ReactNode | undefined;
|
|
154
|
+
chip?: DisclosureChip | undefined;
|
|
155
|
+
failed?: boolean | undefined;
|
|
156
|
+
cancelled?: boolean | undefined;
|
|
157
|
+
expandable?: boolean | undefined;
|
|
158
|
+
defaultOpen?: boolean | undefined;
|
|
159
|
+
/** Compact reel rendering for the rolling status (no chevron, no body). */
|
|
160
|
+
compact?: boolean | undefined;
|
|
161
|
+
accessibilityLabel?: string | undefined;
|
|
162
|
+
children?: ReactNode | undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The one row shape every activity uses, matching the web ActivityDisclosure. */
|
|
166
|
+
export function ActivityDisclosure({
|
|
167
|
+
icon,
|
|
168
|
+
iconTone: iconToneProp = "muted",
|
|
169
|
+
title,
|
|
170
|
+
titleMono,
|
|
171
|
+
running,
|
|
172
|
+
preview,
|
|
173
|
+
media,
|
|
174
|
+
chip: chipProp,
|
|
175
|
+
failed,
|
|
176
|
+
cancelled,
|
|
177
|
+
expandable = true,
|
|
178
|
+
defaultOpen,
|
|
179
|
+
compact,
|
|
180
|
+
accessibilityLabel,
|
|
181
|
+
children,
|
|
182
|
+
}: ActivityDisclosureProps) {
|
|
183
|
+
const theme = useNativeTimelineTheme();
|
|
184
|
+
const [open, setOpen] = useState(defaultOpen ?? false);
|
|
185
|
+
const releaseFollow = useReleaseFollow();
|
|
186
|
+
const iconTone = failed && iconToneProp === "muted" ? "failed" : iconToneProp;
|
|
187
|
+
const chip =
|
|
188
|
+
chipProp ??
|
|
189
|
+
(failed
|
|
190
|
+
? ({ tone: "bad", text: "failed" } as const)
|
|
191
|
+
: cancelled
|
|
192
|
+
? ({ tone: "interrupted", text: "interrupted" } as const)
|
|
193
|
+
: undefined);
|
|
194
|
+
const tint = iconToneColor(theme, iconTone);
|
|
195
|
+
const iconNode =
|
|
196
|
+
typeof icon === "string" ? <Icon name={icon as NativeIconName} size={14} color={tint} /> : icon;
|
|
197
|
+
const titleStyle: TextStyle = titleMono
|
|
198
|
+
? {
|
|
199
|
+
...fontStyle(theme, 400, "mono"),
|
|
200
|
+
fontSize: theme.size.sm,
|
|
201
|
+
lineHeight: 20,
|
|
202
|
+
color: theme.colors["fg-muted"],
|
|
203
|
+
}
|
|
204
|
+
: {
|
|
205
|
+
...fontStyle(theme, 500),
|
|
206
|
+
fontSize: theme.size.base,
|
|
207
|
+
lineHeight: 20,
|
|
208
|
+
color: theme.colors["fg-muted"],
|
|
209
|
+
};
|
|
210
|
+
const previewStyle: TextStyle = {
|
|
211
|
+
...fontStyle(theme),
|
|
212
|
+
fontSize: theme.size.sm,
|
|
213
|
+
lineHeight: 18,
|
|
214
|
+
color: theme.colors["fg-subtle"],
|
|
215
|
+
};
|
|
216
|
+
const titleNode =
|
|
217
|
+
typeof title === "string" ? (
|
|
218
|
+
<ShimmerText style={titleStyle} active={Boolean(running)} numberOfLines={1}>
|
|
219
|
+
{title}
|
|
220
|
+
</ShimmerText>
|
|
221
|
+
) : (
|
|
222
|
+
title
|
|
223
|
+
);
|
|
224
|
+
const previewNode =
|
|
225
|
+
preview == null ? null : typeof preview === "string" ? (
|
|
226
|
+
<Text style={[previewStyle, { flexShrink: 1 }]} numberOfLines={1}>
|
|
227
|
+
{preview}
|
|
228
|
+
</Text>
|
|
229
|
+
) : (
|
|
230
|
+
preview
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (compact) {
|
|
234
|
+
// Web `og-rolling-label`: 13px/450 muted title (never mono), subtle preview.
|
|
235
|
+
const reelTitle: TextStyle = {
|
|
236
|
+
...fontStyle(theme, 400),
|
|
237
|
+
fontSize: 13,
|
|
238
|
+
lineHeight: 20,
|
|
239
|
+
color: theme.colors["fg-muted"],
|
|
240
|
+
};
|
|
241
|
+
const reelPreview: TextStyle = {
|
|
242
|
+
...fontStyle(theme, 400),
|
|
243
|
+
fontSize: 13,
|
|
244
|
+
lineHeight: 20,
|
|
245
|
+
color: theme.colors["fg-subtle"],
|
|
246
|
+
};
|
|
247
|
+
return (
|
|
248
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, minWidth: 0, flex: 1 }}>
|
|
249
|
+
{iconNode}
|
|
250
|
+
<View style={{ flexShrink: 0, maxWidth: preview == null ? "100%" : "75%" }}>
|
|
251
|
+
{typeof title === "string" ? (
|
|
252
|
+
<ShimmerText style={reelTitle} active={Boolean(running)} numberOfLines={1}>
|
|
253
|
+
{title}
|
|
254
|
+
</ShimmerText>
|
|
255
|
+
) : (
|
|
256
|
+
title
|
|
257
|
+
)}
|
|
258
|
+
</View>
|
|
259
|
+
{preview == null ? null : (
|
|
260
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
261
|
+
{typeof preview === "string" ? (
|
|
262
|
+
<Text style={reelPreview} numberOfLines={1}>
|
|
263
|
+
{preview}
|
|
264
|
+
</Text>
|
|
265
|
+
) : (
|
|
266
|
+
preview
|
|
267
|
+
)}
|
|
268
|
+
</View>
|
|
269
|
+
)}
|
|
270
|
+
</View>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const hasBody = expandable && children != null;
|
|
275
|
+
const row = (
|
|
276
|
+
<View
|
|
277
|
+
style={{
|
|
278
|
+
flexDirection: "row",
|
|
279
|
+
alignItems: "center",
|
|
280
|
+
gap: 8,
|
|
281
|
+
minHeight: ROW_MIN_HEIGHT,
|
|
282
|
+
paddingHorizontal: 6,
|
|
283
|
+
paddingVertical: 10,
|
|
284
|
+
borderRadius: theme.radius.sm,
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
{hasBody ? (
|
|
288
|
+
<View style={{ transform: [{ rotate: open ? "90deg" : "0deg" }] }}>
|
|
289
|
+
<Icon name="chevron-right" size={14} color={theme.colors["fg-subtle"]} />
|
|
290
|
+
</View>
|
|
291
|
+
) : (
|
|
292
|
+
<View style={{ width: 14 }} />
|
|
293
|
+
)}
|
|
294
|
+
{iconNode}
|
|
295
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, flex: 1, minWidth: 0 }}>
|
|
296
|
+
<View style={{ flexShrink: 1, minWidth: 0 }}>{titleNode}</View>
|
|
297
|
+
{previewNode && !open && media == null ? (
|
|
298
|
+
<View style={{ flex: 1, minWidth: 0 }}>{previewNode}</View>
|
|
299
|
+
) : null}
|
|
300
|
+
</View>
|
|
301
|
+
{media != null && !open ? (
|
|
302
|
+
<View style={{ paddingLeft: 8, flexShrink: 0 }}>{media}</View>
|
|
303
|
+
) : chip && !open ? (
|
|
304
|
+
<View style={{ paddingLeft: 8 }}>
|
|
305
|
+
<Chip chip={chip} />
|
|
306
|
+
</View>
|
|
307
|
+
) : null}
|
|
308
|
+
</View>
|
|
309
|
+
);
|
|
310
|
+
if (!hasBody) return row;
|
|
311
|
+
return (
|
|
312
|
+
<View>
|
|
313
|
+
<Pressable
|
|
314
|
+
accessibilityRole="button"
|
|
315
|
+
accessibilityState={{ expanded: open }}
|
|
316
|
+
accessibilityLabel={accessibilityLabel ?? (typeof title === "string" ? title : undefined)}
|
|
317
|
+
onPress={() => {
|
|
318
|
+
releaseFollow();
|
|
319
|
+
setOpen((value) => !value);
|
|
320
|
+
}}
|
|
321
|
+
style={({ pressed }) => ({
|
|
322
|
+
borderRadius: theme.radius.sm,
|
|
323
|
+
backgroundColor: pressed ? theme.colors.hover : "transparent",
|
|
324
|
+
})}
|
|
325
|
+
>
|
|
326
|
+
{row}
|
|
327
|
+
</Pressable>
|
|
328
|
+
{open ? (
|
|
329
|
+
<View style={{ marginLeft: 28, marginTop: 6, marginBottom: 8, gap: 8 }}>{children}</View>
|
|
330
|
+
) : null}
|
|
331
|
+
</View>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A running preview: the pulse locus immediately left of the status words. */
|
|
336
|
+
export function RunningPreview({ text, compact }: { text: string; compact?: boolean }) {
|
|
337
|
+
const theme = useNativeTimelineTheme();
|
|
338
|
+
return (
|
|
339
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 6, minWidth: 0 }}>
|
|
340
|
+
{compact ? null : <PulseDot color={theme.colors["status-running"]} />}
|
|
341
|
+
<Text
|
|
342
|
+
numberOfLines={1}
|
|
343
|
+
style={{
|
|
344
|
+
...fontStyle(theme),
|
|
345
|
+
fontSize: compact ? 13 : theme.size.sm,
|
|
346
|
+
lineHeight: compact ? 20 : 18,
|
|
347
|
+
color: theme.colors["fg-subtle"],
|
|
348
|
+
flexShrink: 1,
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
{text}
|
|
352
|
+
</Text>
|
|
353
|
+
</View>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function RuledBlock({
|
|
358
|
+
tone,
|
|
359
|
+
children,
|
|
360
|
+
}: {
|
|
361
|
+
tone: "neutral" | "failed" | "live";
|
|
362
|
+
children: ReactNode;
|
|
363
|
+
}) {
|
|
364
|
+
const theme = useNativeTimelineTheme();
|
|
365
|
+
const borderColor =
|
|
366
|
+
tone === "failed"
|
|
367
|
+
? withAlpha(theme.colors["status-failed"], 0.5)
|
|
368
|
+
: tone === "live"
|
|
369
|
+
? withAlpha(theme.colors["status-running"], 0.5)
|
|
370
|
+
: theme.colors.border;
|
|
371
|
+
return (
|
|
372
|
+
<View
|
|
373
|
+
style={{ borderLeftWidth: 2, borderLeftColor: borderColor, paddingLeft: 12, minWidth: 0 }}
|
|
374
|
+
>
|
|
375
|
+
{children}
|
|
376
|
+
</View>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function TermBlock({
|
|
381
|
+
command,
|
|
382
|
+
workdir,
|
|
383
|
+
output,
|
|
384
|
+
live,
|
|
385
|
+
failed,
|
|
386
|
+
tailLines = 12,
|
|
387
|
+
}: {
|
|
388
|
+
command: string | null;
|
|
389
|
+
workdir?: string | null | undefined;
|
|
390
|
+
output: string;
|
|
391
|
+
live?: boolean | undefined;
|
|
392
|
+
failed?: boolean | undefined;
|
|
393
|
+
tailLines?: number;
|
|
394
|
+
}) {
|
|
395
|
+
const theme = useNativeTimelineTheme();
|
|
396
|
+
const [full, setFull] = useState(false);
|
|
397
|
+
const lines = output.split("\n");
|
|
398
|
+
const big = lines.length > tailLines + 4;
|
|
399
|
+
const shown = full || !big ? output : lines.slice(-tailLines).join("\n");
|
|
400
|
+
const mono: TextStyle = {
|
|
401
|
+
...fontStyle(theme, 400, "mono"),
|
|
402
|
+
fontSize: theme.size.xs,
|
|
403
|
+
lineHeight: 20,
|
|
404
|
+
};
|
|
405
|
+
const showHeader = command != null || Boolean(workdir);
|
|
406
|
+
return (
|
|
407
|
+
<RuledBlock tone={failed ? "failed" : live ? "live" : "neutral"}>
|
|
408
|
+
{showHeader ? (
|
|
409
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingBottom: 4 }}>
|
|
410
|
+
<Text style={[mono, { color: theme.colors["status-idle"] }]}>$</Text>
|
|
411
|
+
<Text
|
|
412
|
+
style={[mono, { fontSize: theme.size.sm, color: theme.colors["fg-muted"], flex: 1 }]}
|
|
413
|
+
numberOfLines={1}
|
|
414
|
+
>
|
|
415
|
+
{command ?? ""}
|
|
416
|
+
</Text>
|
|
417
|
+
{workdir ? (
|
|
418
|
+
<Text
|
|
419
|
+
style={[mono, { color: theme.colors["fg-subtle"], flexShrink: 1 }]}
|
|
420
|
+
numberOfLines={1}
|
|
421
|
+
>
|
|
422
|
+
{workdir}
|
|
423
|
+
</Text>
|
|
424
|
+
) : null}
|
|
425
|
+
</View>
|
|
426
|
+
) : null}
|
|
427
|
+
{output.trim() === "" ? (
|
|
428
|
+
<Text style={[mono, { fontStyle: "italic", color: theme.colors["fg-subtle"] }]}>
|
|
429
|
+
(no output)
|
|
430
|
+
</Text>
|
|
431
|
+
) : (
|
|
432
|
+
<ScrollView style={{ maxHeight: 288 }} nestedScrollEnabled>
|
|
433
|
+
<Text selectable style={[mono, { color: theme.colors["fg-muted"] }]}>
|
|
434
|
+
{shown}
|
|
435
|
+
</Text>
|
|
436
|
+
</ScrollView>
|
|
437
|
+
)}
|
|
438
|
+
{big && !full ? (
|
|
439
|
+
<Pressable onPress={() => setFull(true)} accessibilityRole="button">
|
|
440
|
+
<Text
|
|
441
|
+
style={{
|
|
442
|
+
...fontStyle(theme),
|
|
443
|
+
marginTop: 4,
|
|
444
|
+
fontSize: theme.size.xs,
|
|
445
|
+
color: theme.colors["fg-subtle"],
|
|
446
|
+
}}
|
|
447
|
+
>
|
|
448
|
+
show full output ({lines.length} lines)
|
|
449
|
+
</Text>
|
|
450
|
+
</Pressable>
|
|
451
|
+
) : null}
|
|
452
|
+
</RuledBlock>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function PayloadBlock({
|
|
457
|
+
label,
|
|
458
|
+
value,
|
|
459
|
+
failed,
|
|
460
|
+
}: {
|
|
461
|
+
label: string;
|
|
462
|
+
value: unknown;
|
|
463
|
+
failed?: boolean | undefined;
|
|
464
|
+
}) {
|
|
465
|
+
const theme = useNativeTimelineTheme();
|
|
466
|
+
const text = typeof value === "string" ? value : stringifyPayload(value);
|
|
467
|
+
if (!text || text.trim() === "") return null;
|
|
468
|
+
return (
|
|
469
|
+
<RuledBlock tone={failed ? "failed" : "neutral"}>
|
|
470
|
+
<Text
|
|
471
|
+
style={{
|
|
472
|
+
...fontStyle(theme, 500),
|
|
473
|
+
marginBottom: 4,
|
|
474
|
+
fontSize: theme.size.xs,
|
|
475
|
+
letterSpacing: theme.size.xs * 0.08,
|
|
476
|
+
textTransform: "uppercase",
|
|
477
|
+
color: theme.colors["fg-subtle"],
|
|
478
|
+
}}
|
|
479
|
+
>
|
|
480
|
+
{label}
|
|
481
|
+
</Text>
|
|
482
|
+
<ScrollView style={{ maxHeight: 256 }} nestedScrollEnabled>
|
|
483
|
+
<Text
|
|
484
|
+
selectable
|
|
485
|
+
style={{
|
|
486
|
+
...fontStyle(theme, 400, "mono"),
|
|
487
|
+
fontSize: theme.size.xs,
|
|
488
|
+
lineHeight: 20,
|
|
489
|
+
color: failed ? theme.colors["status-failed"] : theme.colors["fg-muted"],
|
|
490
|
+
}}
|
|
491
|
+
>
|
|
492
|
+
{text}
|
|
493
|
+
</Text>
|
|
494
|
+
</ScrollView>
|
|
495
|
+
</RuledBlock>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function BodyNote({ children, tone }: { children: ReactNode; tone?: "error" | undefined }) {
|
|
500
|
+
const theme = useNativeTimelineTheme();
|
|
501
|
+
if (tone === "error") {
|
|
502
|
+
return (
|
|
503
|
+
<RuledBlock tone="failed">
|
|
504
|
+
<Text
|
|
505
|
+
style={{
|
|
506
|
+
...fontStyle(theme, 400, "mono"),
|
|
507
|
+
fontSize: theme.size.xs,
|
|
508
|
+
lineHeight: 20,
|
|
509
|
+
color: theme.colors["status-failed"],
|
|
510
|
+
}}
|
|
511
|
+
>
|
|
512
|
+
{children}
|
|
513
|
+
</Text>
|
|
514
|
+
</RuledBlock>
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return (
|
|
518
|
+
<Text
|
|
519
|
+
style={{
|
|
520
|
+
...fontStyle(theme),
|
|
521
|
+
paddingHorizontal: 2,
|
|
522
|
+
fontSize: theme.size.sm,
|
|
523
|
+
lineHeight: 20,
|
|
524
|
+
fontStyle: "italic",
|
|
525
|
+
color: theme.colors["fg-subtle"],
|
|
526
|
+
}}
|
|
527
|
+
>
|
|
528
|
+
{children}
|
|
529
|
+
</Text>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** `rgb(r, g, b)` / `#rrggbb` → rgba with the given alpha (Tailwind `/50`). */
|
|
534
|
+
function rgbOf(color: string): [number, number, number] | null {
|
|
535
|
+
const rgb = color.match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/);
|
|
536
|
+
if (rgb) return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
|
|
537
|
+
const hex = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i);
|
|
538
|
+
if (hex) return [parseInt(hex[1]!, 16), parseInt(hex[2]!, 16), parseInt(hex[3]!, 16)];
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* `color` at `alpha` composited over `base`, as an opaque color: a tinted
|
|
544
|
+
* surface that stays solid when it floats over scrolling content.
|
|
545
|
+
*/
|
|
546
|
+
export function blendOver(base: string, color: string, alpha: number): string {
|
|
547
|
+
const under = rgbOf(base);
|
|
548
|
+
const over = rgbOf(color);
|
|
549
|
+
if (!under || !over) return withAlpha(color, alpha);
|
|
550
|
+
const mix = (index: number) => Math.round(under[index]! * (1 - alpha) + over[index]! * alpha);
|
|
551
|
+
return `rgb(${mix(0)}, ${mix(1)}, ${mix(2)})`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function withAlpha(color: string, alpha: number): string {
|
|
555
|
+
const rgb = color.match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/);
|
|
556
|
+
if (rgb) return `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${alpha})`;
|
|
557
|
+
const hex = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i);
|
|
558
|
+
if (hex)
|
|
559
|
+
return `rgba(${parseInt(hex[1]!, 16)}, ${parseInt(hex[2]!, 16)}, ${parseInt(hex[3]!, 16)}, ${alpha})`;
|
|
560
|
+
return color;
|
|
561
|
+
}
|