@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.
Files changed (132) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +74 -0
  3. package/dist/adapters.d.ts +53 -0
  4. package/dist/adapters.js +3 -0
  5. package/dist/adapters.js.map +1 -0
  6. package/dist/attachments.d.ts +75 -0
  7. package/dist/chunk-3VSZAQ7G.js +391 -0
  8. package/dist/chunk-3VSZAQ7G.js.map +1 -0
  9. package/dist/chunk-5ZVK7WKA.js +88 -0
  10. package/dist/chunk-5ZVK7WKA.js.map +1 -0
  11. package/dist/chunk-EEDIMY3L.js +240 -0
  12. package/dist/chunk-EEDIMY3L.js.map +1 -0
  13. package/dist/chunk-M23SDI4R.js +83 -0
  14. package/dist/chunk-M23SDI4R.js.map +1 -0
  15. package/dist/chunk-PHJBSAR3.js +2006 -0
  16. package/dist/chunk-PHJBSAR3.js.map +1 -0
  17. package/dist/environment.d.ts +19 -0
  18. package/dist/expo.d.ts +25 -0
  19. package/dist/expo.js +267 -0
  20. package/dist/expo.js.map +1 -0
  21. package/dist/index.d.ts +13 -0
  22. package/dist/index.js +3345 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/presentation.d.ts +262 -0
  25. package/dist/read-acknowledgement.d.ts +39 -0
  26. package/dist/realtime/call-events.d.ts +11 -0
  27. package/dist/realtime/call-view.d.ts +26 -0
  28. package/dist/realtime/expo-call.d.ts +11 -0
  29. package/dist/realtime/native-call.d.ts +104 -0
  30. package/dist/realtime/native-realtime.d.ts +35 -0
  31. package/dist/realtime/use-native-session-realtime.d.ts +25 -0
  32. package/dist/session-view.d.ts +87 -0
  33. package/dist/timeline/activity.d.ts +34 -0
  34. package/dist/timeline/composer.d.ts +86 -0
  35. package/dist/timeline/controls.d.ts +27 -0
  36. package/dist/timeline/decisions.d.ts +29 -0
  37. package/dist/timeline/feedback.d.ts +26 -0
  38. package/dist/timeline/icon-nodes.generated.d.ts +738 -0
  39. package/dist/timeline/icon.d.ts +11 -0
  40. package/dist/timeline/index.d.ts +32 -0
  41. package/dist/timeline/index.js +5419 -0
  42. package/dist/timeline/index.js.map +1 -0
  43. package/dist/timeline/markdown.d.ts +11 -0
  44. package/dist/timeline/markdown.js +415 -0
  45. package/dist/timeline/markdown.js.map +1 -0
  46. package/dist/timeline/message-attachments.d.ts +13 -0
  47. package/dist/timeline/message-timeline.d.ts +36 -0
  48. package/dist/timeline/model-picker.d.ts +17 -0
  49. package/dist/timeline/primitives.d.ts +85 -0
  50. package/dist/timeline/queue-dock.d.ts +19 -0
  51. package/dist/timeline/session-actions.d.ts +34 -0
  52. package/dist/timeline/session-list.d.ts +50 -0
  53. package/dist/timeline/session-screen.d.ts +35 -0
  54. package/dist/timeline/session-signals.d.ts +37 -0
  55. package/dist/timeline/sheet.d.ts +14 -0
  56. package/dist/timeline/theme.d.ts +58 -0
  57. package/dist/timeline/thinking-orb.d.ts +13 -0
  58. package/dist/timeline/turn.d.ts +80 -0
  59. package/dist/ui/attention.d.ts +14 -0
  60. package/dist/ui/cards.d.ts +21 -0
  61. package/dist/ui/composer.d.ts +21 -0
  62. package/dist/ui/format.d.ts +13 -0
  63. package/dist/ui/icons.d.ts +9 -0
  64. package/dist/ui/index.d.ts +12 -0
  65. package/dist/ui/index.js +1757 -0
  66. package/dist/ui/index.js.map +1 -0
  67. package/dist/ui/steps-sheet.d.ts +10 -0
  68. package/dist/ui/theme.d.ts +63 -0
  69. package/dist/ui/timeline.d.ts +48 -0
  70. package/dist/ui/web-tokens.gen.d.ts +90 -0
  71. package/dist/use-native-session.d.ts +40 -0
  72. package/dist/view-model.d.ts +52 -0
  73. package/dist/voice-input.d.ts +102 -0
  74. package/dist/webrtc.d.ts +9 -0
  75. package/dist/webrtc.js +19 -0
  76. package/dist/webrtc.js.map +1 -0
  77. package/expo-module.config.json +7 -0
  78. package/ios/OpenGeniCall.podspec +22 -0
  79. package/ios/OpenGeniCallAppDelegateSubscriber.swift +29 -0
  80. package/ios/OpenGeniCallCenter.swift +189 -0
  81. package/ios/OpenGeniCallModule.swift +64 -0
  82. package/package.json +93 -0
  83. package/src/adapters.ts +150 -0
  84. package/src/attachments.ts +396 -0
  85. package/src/environment.tsx +214 -0
  86. package/src/expo.ts +295 -0
  87. package/src/index.ts +98 -0
  88. package/src/presentation.ts +285 -0
  89. package/src/read-acknowledgement.ts +72 -0
  90. package/src/realtime/call-events.ts +41 -0
  91. package/src/realtime/call-view.tsx +220 -0
  92. package/src/realtime/expo-call.ts +49 -0
  93. package/src/realtime/native-call.ts +266 -0
  94. package/src/realtime/native-realtime.ts +115 -0
  95. package/src/realtime/use-native-session-realtime.ts +29 -0
  96. package/src/session-view.tsx +2251 -0
  97. package/src/timeline/activity.tsx +736 -0
  98. package/src/timeline/composer.tsx +686 -0
  99. package/src/timeline/controls.tsx +151 -0
  100. package/src/timeline/decisions.tsx +651 -0
  101. package/src/timeline/feedback.tsx +251 -0
  102. package/src/timeline/icon-nodes.generated.ts +1830 -0
  103. package/src/timeline/icon.tsx +44 -0
  104. package/src/timeline/index.ts +67 -0
  105. package/src/timeline/markdown.tsx +510 -0
  106. package/src/timeline/message-attachments.tsx +155 -0
  107. package/src/timeline/message-timeline.tsx +1042 -0
  108. package/src/timeline/model-picker.tsx +450 -0
  109. package/src/timeline/primitives.tsx +561 -0
  110. package/src/timeline/queue-dock.tsx +331 -0
  111. package/src/timeline/session-actions.tsx +379 -0
  112. package/src/timeline/session-list.tsx +338 -0
  113. package/src/timeline/session-screen.tsx +488 -0
  114. package/src/timeline/session-signals.tsx +312 -0
  115. package/src/timeline/sheet.tsx +153 -0
  116. package/src/timeline/theme.tsx +128 -0
  117. package/src/timeline/thinking-orb.tsx +93 -0
  118. package/src/timeline/turn.tsx +569 -0
  119. package/src/ui/attention.tsx +220 -0
  120. package/src/ui/cards.tsx +267 -0
  121. package/src/ui/composer.tsx +296 -0
  122. package/src/ui/format.ts +86 -0
  123. package/src/ui/icons.tsx +131 -0
  124. package/src/ui/index.ts +18 -0
  125. package/src/ui/steps-sheet.tsx +71 -0
  126. package/src/ui/theme.ts +261 -0
  127. package/src/ui/timeline.tsx +837 -0
  128. package/src/ui/web-tokens.gen.ts +94 -0
  129. package/src/use-native-session.ts +219 -0
  130. package/src/view-model.ts +485 -0
  131. package/src/voice-input.ts +382 -0
  132. 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
+ }