@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,44 @@
|
|
|
1
|
+
import Svg, { Circle, Line, Path, Polyline, Rect } from "react-native-svg";
|
|
2
|
+
import { iconNodes, type NativeIconName } from "./icon-nodes.generated";
|
|
3
|
+
|
|
4
|
+
export type { NativeIconName };
|
|
5
|
+
|
|
6
|
+
const ELEMENTS = { path: Path, circle: Circle, rect: Rect, line: Line, polyline: Polyline };
|
|
7
|
+
|
|
8
|
+
/** One lucide glyph with the web's geometry (24 viewBox, 2px round stroke). */
|
|
9
|
+
export function Icon({
|
|
10
|
+
name,
|
|
11
|
+
size = 14,
|
|
12
|
+
color,
|
|
13
|
+
strokeWidth = 2,
|
|
14
|
+
fill,
|
|
15
|
+
}: {
|
|
16
|
+
name: NativeIconName;
|
|
17
|
+
size?: number;
|
|
18
|
+
color: string;
|
|
19
|
+
strokeWidth?: number;
|
|
20
|
+
/** Solid glyphs (the web fills pause/play). */
|
|
21
|
+
fill?: string | undefined;
|
|
22
|
+
}) {
|
|
23
|
+
const node = iconNodes[name] as ReadonlyArray<readonly [string, Record<string, string | number>]>;
|
|
24
|
+
return (
|
|
25
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
26
|
+
{node.map(([tag, attrs]) => {
|
|
27
|
+
const Element = ELEMENTS[tag as keyof typeof ELEMENTS];
|
|
28
|
+
if (!Element) return null;
|
|
29
|
+
const props = { ...attrs } as Record<string, unknown>;
|
|
30
|
+
return (
|
|
31
|
+
<Element
|
|
32
|
+
key={`${tag}:${JSON.stringify(attrs)}`}
|
|
33
|
+
{...props}
|
|
34
|
+
stroke={color}
|
|
35
|
+
strokeWidth={strokeWidth}
|
|
36
|
+
strokeLinecap="round"
|
|
37
|
+
strokeLinejoin="round"
|
|
38
|
+
fill={fill ?? "none"}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
})}
|
|
42
|
+
</Svg>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @opengeni/react-native/timeline: the web session timeline drawn with native
|
|
2
|
+
// primitives. Decisions come from @opengeni/react/timeline-model, styles from the
|
|
3
|
+
// generated web tokens; hosts customize through theme overrides and slots.
|
|
4
|
+
export { MessageTimeline } from "./message-timeline";
|
|
5
|
+
export type { NativeMarkdownRenderer, NativeMessageTimelineProps } from "./message-timeline";
|
|
6
|
+
export { ActivityRow, PresentedToolRow } from "./activity";
|
|
7
|
+
export type {
|
|
8
|
+
NativeActivityOptions,
|
|
9
|
+
NativeToolRenderer,
|
|
10
|
+
NativeToolRendererProps,
|
|
11
|
+
} from "./activity";
|
|
12
|
+
export { ActivityRail, PreparingState, RollingActivity, TurnRailFrame, TurnSummary } from "./turn";
|
|
13
|
+
export type { NativeTurnStatus, PreparingProps } from "./turn";
|
|
14
|
+
export {
|
|
15
|
+
ActivityDisclosure,
|
|
16
|
+
BodyNote,
|
|
17
|
+
Chip,
|
|
18
|
+
PayloadBlock,
|
|
19
|
+
PulseDot,
|
|
20
|
+
ShimmerText,
|
|
21
|
+
TermBlock,
|
|
22
|
+
withAlpha,
|
|
23
|
+
} from "./primitives";
|
|
24
|
+
export { Icon } from "./icon";
|
|
25
|
+
export type { NativeIconName } from "./icon";
|
|
26
|
+
export {
|
|
27
|
+
createNativeTimelineTheme,
|
|
28
|
+
fontStyle,
|
|
29
|
+
NativeTimelineThemeProvider,
|
|
30
|
+
useNativeTimelineTheme,
|
|
31
|
+
} from "./theme";
|
|
32
|
+
export type {
|
|
33
|
+
NativeTimelineColors,
|
|
34
|
+
NativeTimelineFonts,
|
|
35
|
+
NativeTimelineTheme,
|
|
36
|
+
NativeTimelineThemeOverrides,
|
|
37
|
+
WebColorToken,
|
|
38
|
+
} from "./theme";
|
|
39
|
+
export { AttachmentChips, NativeSessionScreen } from "./session-screen";
|
|
40
|
+
export type { NativeSessionScreenProps } from "./session-screen";
|
|
41
|
+
export { ComposerPill, SessionComposer } from "./composer";
|
|
42
|
+
export { ModelPickerSheet } from "./model-picker";
|
|
43
|
+
export { BottomSheet } from "./sheet";
|
|
44
|
+
export { SessionActionsButton } from "./session-actions";
|
|
45
|
+
export type { SessionAction, SessionActionsClient, SessionActionsProps } from "./session-actions";
|
|
46
|
+
export type { ModelPickerSheetProps } from "./model-picker";
|
|
47
|
+
export type { SessionComposerProps } from "./composer";
|
|
48
|
+
export { ApprovalStrip, defaultApprovalStripMessages, HumanInputCard } from "./decisions";
|
|
49
|
+
export type { ApprovalStripMessages, ApprovalStripProps, HumanInputCardProps } from "./decisions";
|
|
50
|
+
export { Button, IconButton } from "./controls";
|
|
51
|
+
export type { ButtonVariant } from "./controls";
|
|
52
|
+
export { NativeMessageAttachments } from "./message-attachments";
|
|
53
|
+
export { QueueDock } from "./queue-dock";
|
|
54
|
+
export { SessionCommandsList, SessionSignals } from "./session-signals";
|
|
55
|
+
export { FeedbackSheet, TurnFeedbackButtons, useTurnRatings } from "./feedback";
|
|
56
|
+
export type { TurnFeedbackTarget } from "./feedback";
|
|
57
|
+
export {
|
|
58
|
+
ModelMark,
|
|
59
|
+
SectionLabel,
|
|
60
|
+
SessionRow,
|
|
61
|
+
SessionRowList,
|
|
62
|
+
SessionHeaderTitle,
|
|
63
|
+
SessionStatusBadge,
|
|
64
|
+
StatusDot,
|
|
65
|
+
} from "./session-list";
|
|
66
|
+
export type { SessionRowProps } from "./session-list";
|
|
67
|
+
export { webColorsDark, webColorsLight } from "../ui/web-tokens.gen";
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, useState, type ReactNode } from "react";
|
|
2
|
+
import { Pressable, ScrollView, Text, View, type TextStyle } from "react-native";
|
|
3
|
+
import Markdown, { MarkdownIt, renderRules, type RenderRules } from "react-native-markdown-display";
|
|
4
|
+
import { isReservedOpenGeniLink, parseOpenGeniLink, type OpenGeniLinkTarget } from "@opengeni/sdk";
|
|
5
|
+
import { Icon } from "./icon";
|
|
6
|
+
import { withAlpha as withAlphaColor } from "./primitives";
|
|
7
|
+
import type { NativeMarkdownRenderer } from "./message-timeline";
|
|
8
|
+
import {
|
|
9
|
+
fontStyle,
|
|
10
|
+
insetSurface,
|
|
11
|
+
MONO_FALLBACK,
|
|
12
|
+
useNativeTimelineTheme,
|
|
13
|
+
type NativeTimelineTheme,
|
|
14
|
+
} from "./theme";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One parser for every message, configured like the web's `remark-gfm`: no typographic
|
|
18
|
+
* substitution (quotes, dashes and `(c)` stay as written), and bare `http(s)://` URLs
|
|
19
|
+
* autolink while file-like words (`fib.py`) do not. The library default would build a
|
|
20
|
+
* new typographer parser on every render, including every streaming delta.
|
|
21
|
+
*/
|
|
22
|
+
const PARSER = (() => {
|
|
23
|
+
const parser = MarkdownIt({ typographer: false, linkify: true });
|
|
24
|
+
parser.linkify.set({ fuzzyLink: false, fuzzyEmail: false });
|
|
25
|
+
return parser;
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
/* ----------------------------------------------------------------------------
|
|
29
|
+
Web Markdown (components/markdown.tsx) styles for react-native-markdown-display.
|
|
30
|
+
Prose 15/28 (`text-og-md leading-7`), paragraphs `my-2.5`, lists `ml-5 gap-1`,
|
|
31
|
+
code blocks `rounded-og-md bg-surface-1/70 px-3 py-2.5 text-og-sm leading-5`.
|
|
32
|
+
-------------------------------------------------------------------------- */
|
|
33
|
+
|
|
34
|
+
export function webMarkdownStyles(
|
|
35
|
+
theme: NativeTimelineTheme,
|
|
36
|
+
tone: "body" | "muted",
|
|
37
|
+
): Record<string, TextStyle> {
|
|
38
|
+
const c = theme.colors;
|
|
39
|
+
const muted = tone === "muted";
|
|
40
|
+
const size = muted ? theme.size.base : theme.size.md;
|
|
41
|
+
const line = muted ? 24 : 28;
|
|
42
|
+
const body = {
|
|
43
|
+
...fontStyle(theme),
|
|
44
|
+
color: muted ? c["fg-muted"] : c.fg,
|
|
45
|
+
fontSize: size,
|
|
46
|
+
lineHeight: line,
|
|
47
|
+
};
|
|
48
|
+
const mono = fontStyle(theme, 400, "mono");
|
|
49
|
+
const heading = { ...fontStyle(theme, 600), color: c.fg, letterSpacing: -0.2 };
|
|
50
|
+
return {
|
|
51
|
+
body,
|
|
52
|
+
paragraph: { marginTop: 0, marginBottom: 10 },
|
|
53
|
+
// Web margins collapse against the 10pt paragraph margin; native margins add up.
|
|
54
|
+
heading1: { ...heading, fontSize: 20, lineHeight: 28, marginTop: 10, marginBottom: 10 },
|
|
55
|
+
heading2: { ...heading, fontSize: 18, lineHeight: 26, marginTop: 10, marginBottom: 8 },
|
|
56
|
+
heading3: {
|
|
57
|
+
...heading,
|
|
58
|
+
fontSize: theme.size.md,
|
|
59
|
+
lineHeight: 24,
|
|
60
|
+
marginTop: 6,
|
|
61
|
+
marginBottom: 6,
|
|
62
|
+
},
|
|
63
|
+
heading4: {
|
|
64
|
+
...heading,
|
|
65
|
+
fontSize: theme.size.sm,
|
|
66
|
+
lineHeight: 18,
|
|
67
|
+
marginTop: 6,
|
|
68
|
+
marginBottom: 6,
|
|
69
|
+
textTransform: "uppercase" as const,
|
|
70
|
+
},
|
|
71
|
+
strong: { ...fontStyle(theme, 600), color: muted ? c["fg-muted"] : c.fg },
|
|
72
|
+
em: theme.fonts.sansItalic
|
|
73
|
+
? { fontFamily: theme.fonts.sansItalic }
|
|
74
|
+
: { fontStyle: "italic" as const },
|
|
75
|
+
bullet_list: { marginTop: 0, marginBottom: 10 },
|
|
76
|
+
ordered_list: { marginTop: 0, marginBottom: 10 },
|
|
77
|
+
list_item: { marginBottom: 4 },
|
|
78
|
+
bullet_list_icon: { color: c["fg-subtle"], marginLeft: 6, marginRight: 8, lineHeight: line },
|
|
79
|
+
ordered_list_icon: { color: c["fg-subtle"], marginLeft: 2, marginRight: 6, lineHeight: line },
|
|
80
|
+
code_inline: {
|
|
81
|
+
...mono,
|
|
82
|
+
fontFamily: (mono.fontFamily as string | undefined) ?? MONO_FALLBACK,
|
|
83
|
+
fontSize: theme.size.sm,
|
|
84
|
+
backgroundColor: c["surface-2"],
|
|
85
|
+
color: c.fg,
|
|
86
|
+
borderRadius: theme.radius.xs,
|
|
87
|
+
paddingHorizontal: 4,
|
|
88
|
+
borderWidth: 0,
|
|
89
|
+
},
|
|
90
|
+
fence: {
|
|
91
|
+
...mono,
|
|
92
|
+
fontSize: theme.size.sm,
|
|
93
|
+
lineHeight: 20,
|
|
94
|
+
color: c["fg-muted"],
|
|
95
|
+
backgroundColor: insetSurface(theme),
|
|
96
|
+
borderWidth: 0,
|
|
97
|
+
borderRadius: theme.radius.md,
|
|
98
|
+
paddingHorizontal: 12,
|
|
99
|
+
paddingVertical: 10,
|
|
100
|
+
marginTop: 2,
|
|
101
|
+
marginBottom: 10,
|
|
102
|
+
},
|
|
103
|
+
code_block: {
|
|
104
|
+
...mono,
|
|
105
|
+
fontSize: theme.size.sm,
|
|
106
|
+
lineHeight: 20,
|
|
107
|
+
color: c["fg-muted"],
|
|
108
|
+
backgroundColor: insetSurface(theme),
|
|
109
|
+
borderWidth: 0,
|
|
110
|
+
borderRadius: theme.radius.md,
|
|
111
|
+
paddingHorizontal: 12,
|
|
112
|
+
paddingVertical: 10,
|
|
113
|
+
},
|
|
114
|
+
// Web MARKDOWN_LINK_CLASS: medium weight in accent-strong, no underline.
|
|
115
|
+
link: {
|
|
116
|
+
...fontStyle(theme, 500),
|
|
117
|
+
color: c["accent-strong"],
|
|
118
|
+
textDecorationLine: "none" as const,
|
|
119
|
+
},
|
|
120
|
+
blockquote: {
|
|
121
|
+
// Web blockquote text is fg-muted; text styles inherit through the renderer.
|
|
122
|
+
color: c["fg-muted"],
|
|
123
|
+
backgroundColor: "transparent",
|
|
124
|
+
borderLeftColor: c["border-strong"],
|
|
125
|
+
borderLeftWidth: 2,
|
|
126
|
+
paddingLeft: 14,
|
|
127
|
+
marginLeft: 0,
|
|
128
|
+
marginVertical: 12,
|
|
129
|
+
},
|
|
130
|
+
hr: { backgroundColor: c.border, height: 1, marginVertical: 16 },
|
|
131
|
+
// Web tables: unboxed, text-og-base, hairline rules, content-sized columns.
|
|
132
|
+
table: { borderWidth: 0, marginTop: 12, marginBottom: 10 },
|
|
133
|
+
thead: {},
|
|
134
|
+
th: {
|
|
135
|
+
...fontStyle(theme, 500),
|
|
136
|
+
color: c.fg,
|
|
137
|
+
fontSize: theme.size.base,
|
|
138
|
+
lineHeight: 20,
|
|
139
|
+
paddingVertical: 6,
|
|
140
|
+
paddingRight: 16,
|
|
141
|
+
},
|
|
142
|
+
tr: { borderBottomWidth: 0 },
|
|
143
|
+
td: {
|
|
144
|
+
color: c["fg-muted"],
|
|
145
|
+
fontSize: theme.size.base,
|
|
146
|
+
lineHeight: 20,
|
|
147
|
+
paddingVertical: 6,
|
|
148
|
+
paddingRight: 16,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function CodeFence({
|
|
154
|
+
content,
|
|
155
|
+
language,
|
|
156
|
+
onCopy,
|
|
157
|
+
}: {
|
|
158
|
+
content: string;
|
|
159
|
+
language: string;
|
|
160
|
+
onCopy?: ((text: string) => void) | undefined;
|
|
161
|
+
}) {
|
|
162
|
+
const theme = useNativeTimelineTheme();
|
|
163
|
+
const styles = webMarkdownStyles(theme, "body");
|
|
164
|
+
const [copied, setCopied] = useState(false);
|
|
165
|
+
// The code scrolls in the space left of the language label and copy button, which
|
|
166
|
+
// vary in width ("SH" vs "TYPESCRIPT"); reserve their measured width plus a gap.
|
|
167
|
+
const [chromeWidth, setChromeWidth] = useState(64);
|
|
168
|
+
return (
|
|
169
|
+
<View style={{ marginTop: 2, marginBottom: 10 }}>
|
|
170
|
+
<View
|
|
171
|
+
style={{
|
|
172
|
+
borderRadius: theme.radius.md,
|
|
173
|
+
backgroundColor: insetSurface(theme),
|
|
174
|
+
paddingLeft: 12,
|
|
175
|
+
paddingRight: chromeWidth,
|
|
176
|
+
paddingVertical: 10,
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
180
|
+
<Text
|
|
181
|
+
selectable
|
|
182
|
+
style={{
|
|
183
|
+
...styles.code_block,
|
|
184
|
+
backgroundColor: "transparent",
|
|
185
|
+
paddingHorizontal: 0,
|
|
186
|
+
paddingVertical: 0,
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{content}
|
|
190
|
+
</Text>
|
|
191
|
+
</ScrollView>
|
|
192
|
+
</View>
|
|
193
|
+
<View
|
|
194
|
+
onLayout={(event) => {
|
|
195
|
+
const next = Math.ceil(event.nativeEvent.layout.width) + 6 + 10;
|
|
196
|
+
if (next !== chromeWidth) setChromeWidth(next);
|
|
197
|
+
}}
|
|
198
|
+
style={{
|
|
199
|
+
position: "absolute",
|
|
200
|
+
top: 6,
|
|
201
|
+
right: 6,
|
|
202
|
+
flexDirection: "row",
|
|
203
|
+
alignItems: "center",
|
|
204
|
+
gap: 4,
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
{language ? (
|
|
208
|
+
<Text
|
|
209
|
+
style={{
|
|
210
|
+
...fontStyle(theme, 400, "mono"),
|
|
211
|
+
fontSize: 10,
|
|
212
|
+
letterSpacing: 0.5,
|
|
213
|
+
textTransform: "uppercase",
|
|
214
|
+
color: theme.colors["fg-subtle"],
|
|
215
|
+
opacity: 0.7,
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
{language}
|
|
219
|
+
</Text>
|
|
220
|
+
) : null}
|
|
221
|
+
{onCopy ? (
|
|
222
|
+
<Pressable
|
|
223
|
+
accessibilityRole="button"
|
|
224
|
+
accessibilityLabel="Copy code"
|
|
225
|
+
hitSlop={8}
|
|
226
|
+
onPress={() => {
|
|
227
|
+
onCopy(content);
|
|
228
|
+
setCopied(true);
|
|
229
|
+
setTimeout(() => setCopied(false), 1400);
|
|
230
|
+
}}
|
|
231
|
+
style={{ width: 28, height: 28, alignItems: "center", justifyContent: "center" }}
|
|
232
|
+
>
|
|
233
|
+
<Icon name={copied ? "check" : "copy"} size={14} color={theme.colors["fg-subtle"]} />
|
|
234
|
+
</Pressable>
|
|
235
|
+
) : null}
|
|
236
|
+
</View>
|
|
237
|
+
</View>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* Tables ------------------------------------------------------------------- */
|
|
242
|
+
|
|
243
|
+
type AstNode = { type: string; content?: string; children?: AstNode[] };
|
|
244
|
+
|
|
245
|
+
function astText(node: AstNode): string {
|
|
246
|
+
// Container nodes carry an empty content string; text lives in their leaves.
|
|
247
|
+
if (node.children && node.children.length > 0) return node.children.map(astText).join("");
|
|
248
|
+
return typeof node.content === "string" ? node.content : "";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function tableRows(table: AstNode): string[][] {
|
|
252
|
+
const rows: string[][] = [];
|
|
253
|
+
const visit = (node: AstNode) => {
|
|
254
|
+
if (node.type === "tr") rows.push((node.children ?? []).map((cell) => astText(cell).trim()));
|
|
255
|
+
else (node.children ?? []).forEach(visit);
|
|
256
|
+
};
|
|
257
|
+
visit(table);
|
|
258
|
+
return rows;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Content-sized columns, as a browser's auto table layout lays them out: short
|
|
263
|
+
* columns keep their natural width on one line, long ones share the rest.
|
|
264
|
+
*/
|
|
265
|
+
function tableColumns(rows: string[][]): { width?: number; grow: number }[] {
|
|
266
|
+
const count = Math.max(0, ...rows.map((row) => row.length));
|
|
267
|
+
return Array.from({ length: count }, (_, index) => {
|
|
268
|
+
const longest = Math.max(1, ...rows.map((row) => row[index]?.length ?? 0));
|
|
269
|
+
return longest <= 14 ? { width: Math.ceil(longest * 8.4) + 20, grow: 0 } : { grow: longest };
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** A cell's column: its sibling index in the row (set by the AST builder). */
|
|
274
|
+
function cellIndex(node: { index?: unknown }): number {
|
|
275
|
+
return typeof node.index === "number" && node.index >= 0 ? node.index : 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Explicit column widths for a measured table; empty until the table lays out. */
|
|
279
|
+
const TableColumnsContext = createContext<number[]>([]);
|
|
280
|
+
|
|
281
|
+
function columnWidths(columns: { width?: number; grow: number }[], available: number): number[] {
|
|
282
|
+
const fixed = columns.reduce((sum, column) => sum + (column.width ?? 0), 0);
|
|
283
|
+
const growth = columns.reduce(
|
|
284
|
+
(sum, column) => sum + (column.width === undefined ? column.grow : 0),
|
|
285
|
+
0,
|
|
286
|
+
);
|
|
287
|
+
const rest = Math.max(0, available - fixed);
|
|
288
|
+
if (growth === 0) return columns.map((column) => column.width ?? 0);
|
|
289
|
+
// Long columns share the rest by length, each keeping a readable minimum.
|
|
290
|
+
return columns.map((column) =>
|
|
291
|
+
column.width !== undefined ? column.width : Math.max(64, (rest * column.grow) / growth),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function MarkdownTable({
|
|
296
|
+
node,
|
|
297
|
+
children,
|
|
298
|
+
onCopy,
|
|
299
|
+
}: {
|
|
300
|
+
node: AstNode;
|
|
301
|
+
children: ReactNode;
|
|
302
|
+
onCopy?: ((text: string) => void) | undefined;
|
|
303
|
+
}) {
|
|
304
|
+
const theme = useNativeTimelineTheme();
|
|
305
|
+
const rows = useMemo(() => tableRows(node), [node]);
|
|
306
|
+
const columns = useMemo(() => tableColumns(rows), [rows]);
|
|
307
|
+
const [available, setAvailable] = useState(0);
|
|
308
|
+
const widths = useMemo(
|
|
309
|
+
() => (available > 0 ? columnWidths(columns, available) : []),
|
|
310
|
+
[available, columns],
|
|
311
|
+
);
|
|
312
|
+
const [copied, setCopied] = useState(false);
|
|
313
|
+
return (
|
|
314
|
+
<View
|
|
315
|
+
style={{ marginTop: 12, marginBottom: 10 }}
|
|
316
|
+
onLayout={(event) => setAvailable(Math.round(event.nativeEvent.layout.width))}
|
|
317
|
+
>
|
|
318
|
+
<TableColumnsContext.Provider value={widths}>{children}</TableColumnsContext.Provider>
|
|
319
|
+
{onCopy ? (
|
|
320
|
+
<Pressable
|
|
321
|
+
accessibilityRole="button"
|
|
322
|
+
accessibilityLabel="Copy table"
|
|
323
|
+
hitSlop={8}
|
|
324
|
+
onPress={() => {
|
|
325
|
+
onCopy(rows.map((row) => row.join("\t")).join("\n"));
|
|
326
|
+
setCopied(true);
|
|
327
|
+
setTimeout(() => setCopied(false), 1400);
|
|
328
|
+
}}
|
|
329
|
+
style={{
|
|
330
|
+
position: "absolute",
|
|
331
|
+
top: 2,
|
|
332
|
+
right: 0,
|
|
333
|
+
width: 28,
|
|
334
|
+
height: 28,
|
|
335
|
+
alignItems: "center",
|
|
336
|
+
justifyContent: "center",
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
<Icon name={copied ? "check" : "copy"} size={14} color={theme.colors["fg-subtle"]} />
|
|
340
|
+
</Pressable>
|
|
341
|
+
) : null}
|
|
342
|
+
</View>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function TableRow({ head, last, children }: { head: boolean; last: boolean; children: ReactNode }) {
|
|
347
|
+
const theme = useNativeTimelineTheme();
|
|
348
|
+
const c = theme.colors;
|
|
349
|
+
return (
|
|
350
|
+
<View
|
|
351
|
+
style={{
|
|
352
|
+
flexDirection: "row",
|
|
353
|
+
borderBottomWidth: last ? 0 : 1,
|
|
354
|
+
// Header rule in border; body rules at 70% (web border-og-border/70).
|
|
355
|
+
borderBottomColor: head ? c.border : withAlphaColor(c.border, 0.7),
|
|
356
|
+
}}
|
|
357
|
+
>
|
|
358
|
+
{children}
|
|
359
|
+
</View>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function TableCell({
|
|
364
|
+
index,
|
|
365
|
+
style,
|
|
366
|
+
children,
|
|
367
|
+
}: {
|
|
368
|
+
index: number;
|
|
369
|
+
style: object;
|
|
370
|
+
children: ReactNode;
|
|
371
|
+
}) {
|
|
372
|
+
const width = useContext(TableColumnsContext)[index];
|
|
373
|
+
return (
|
|
374
|
+
<View
|
|
375
|
+
style={[
|
|
376
|
+
style,
|
|
377
|
+
// The renderer's default cell style is flex: 1 (equal columns); a measured
|
|
378
|
+
// table replaces it with content-sized widths.
|
|
379
|
+
width !== undefined ? { flex: 0, width } : null,
|
|
380
|
+
]}
|
|
381
|
+
>
|
|
382
|
+
{children}
|
|
383
|
+
</View>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function webRules(
|
|
388
|
+
theme: NativeTimelineTheme,
|
|
389
|
+
tone: "body" | "muted",
|
|
390
|
+
onCopy?: (text: string) => void,
|
|
391
|
+
): RenderRules {
|
|
392
|
+
const line = tone === "muted" ? 24 : 28;
|
|
393
|
+
return {
|
|
394
|
+
...renderRules,
|
|
395
|
+
// Web lists use a disc marker in fg-subtle; the library's iOS default is a middle dot.
|
|
396
|
+
list_item: (node, children, parent, styles, inheritedStyles = {}) => {
|
|
397
|
+
if (parent.some((entry: { type: string }) => entry.type === "bullet_list")) {
|
|
398
|
+
// A drawn disc: the text bullet glyph renders noticeably smaller than
|
|
399
|
+
// the browser's list-disc marker at the same line height.
|
|
400
|
+
const disc = 5.5;
|
|
401
|
+
return (
|
|
402
|
+
<View key={node.key} style={{ flexDirection: "row", marginBottom: 4 }}>
|
|
403
|
+
<View
|
|
404
|
+
accessible={false}
|
|
405
|
+
style={{ width: 20, height: line, alignItems: "center", justifyContent: "center" }}
|
|
406
|
+
>
|
|
407
|
+
<View
|
|
408
|
+
style={{
|
|
409
|
+
width: disc,
|
|
410
|
+
height: disc,
|
|
411
|
+
borderRadius: disc / 2,
|
|
412
|
+
backgroundColor: theme.colors["fg-subtle"],
|
|
413
|
+
}}
|
|
414
|
+
/>
|
|
415
|
+
</View>
|
|
416
|
+
<View style={{ flex: 1, minWidth: 0 }}>{children}</View>
|
|
417
|
+
</View>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
return renderRules.list_item!(node, children, parent, styles, inheritedStyles);
|
|
421
|
+
},
|
|
422
|
+
table: (node, children) => (
|
|
423
|
+
<MarkdownTable key={node.key} node={node} onCopy={onCopy}>
|
|
424
|
+
{children}
|
|
425
|
+
</MarkdownTable>
|
|
426
|
+
),
|
|
427
|
+
tr: (node, children, parent) => {
|
|
428
|
+
const head = parent.some((entry: { type: string }) => entry.type === "thead");
|
|
429
|
+
const body = parent.find((entry: { type: string }) => entry.type === "tbody") as
|
|
430
|
+
| { children?: unknown[] }
|
|
431
|
+
| undefined;
|
|
432
|
+
const last = !head && body?.children?.[body.children.length - 1] === node;
|
|
433
|
+
return (
|
|
434
|
+
<TableRow key={node.key} head={head} last={last}>
|
|
435
|
+
{children}
|
|
436
|
+
</TableRow>
|
|
437
|
+
);
|
|
438
|
+
},
|
|
439
|
+
th: (node, children, _parent, styles) => (
|
|
440
|
+
<TableCell key={node.key} index={cellIndex(node)} style={styles._VIEW_SAFE_th}>
|
|
441
|
+
{children}
|
|
442
|
+
</TableCell>
|
|
443
|
+
),
|
|
444
|
+
td: (node, children, _parent, styles) => (
|
|
445
|
+
<TableCell key={node.key} index={cellIndex(node)} style={styles._VIEW_SAFE_td}>
|
|
446
|
+
{children}
|
|
447
|
+
</TableCell>
|
|
448
|
+
),
|
|
449
|
+
fence: (node) => {
|
|
450
|
+
const raw = typeof node.content === "string" ? node.content : "";
|
|
451
|
+
const content = raw.endsWith("\n") ? raw.slice(0, -1) : raw;
|
|
452
|
+
const info = (node as { sourceInfo?: unknown }).sourceInfo;
|
|
453
|
+
const language = typeof info === "string" ? (info.trim().split(/\s+/)[0] ?? "") : "";
|
|
454
|
+
return <CodeFence key={node.key} content={content} language={language} onCopy={onCopy} />;
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Trim the trailing paragraph margin (web `last:mb-0`). */
|
|
460
|
+
function TrimmedMarkdown({
|
|
461
|
+
text,
|
|
462
|
+
tone,
|
|
463
|
+
onLinkPress,
|
|
464
|
+
onCopy,
|
|
465
|
+
}: {
|
|
466
|
+
text: string;
|
|
467
|
+
tone: "body" | "muted";
|
|
468
|
+
onLinkPress?: ((url: string) => boolean) | undefined;
|
|
469
|
+
onCopy?: ((text: string) => void) | undefined;
|
|
470
|
+
}) {
|
|
471
|
+
const theme = useNativeTimelineTheme();
|
|
472
|
+
const styles = useMemo(() => webMarkdownStyles(theme, tone), [theme, tone]);
|
|
473
|
+
const rules = useMemo(() => webRules(theme, tone, onCopy), [theme, tone, onCopy]);
|
|
474
|
+
return (
|
|
475
|
+
<View style={{ marginBottom: -10 }}>
|
|
476
|
+
<Markdown
|
|
477
|
+
markdownit={PARSER}
|
|
478
|
+
style={styles}
|
|
479
|
+
rules={rules}
|
|
480
|
+
{...(onLinkPress ? { onLinkPress } : {})}
|
|
481
|
+
>
|
|
482
|
+
{text}
|
|
483
|
+
</Markdown>
|
|
484
|
+
</View>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function createWebMarkdownRenderer(
|
|
489
|
+
options: {
|
|
490
|
+
onLinkPress?: (url: string) => boolean;
|
|
491
|
+
onCopy?: (text: string) => void;
|
|
492
|
+
/** OpenGeni links (sandbox files, artifacts, Sites); never handed to the OS. */
|
|
493
|
+
onOpenGeniLink?: (target: OpenGeniLinkTarget) => void;
|
|
494
|
+
} = {},
|
|
495
|
+
): NativeMarkdownRenderer {
|
|
496
|
+
const onLinkPress = (url: string): boolean => {
|
|
497
|
+
const target = parseOpenGeniLink(url);
|
|
498
|
+
if (target || isReservedOpenGeniLink(url)) {
|
|
499
|
+
if (target) options.onOpenGeniLink?.(target);
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
return options.onLinkPress ? options.onLinkPress(url) : true;
|
|
503
|
+
};
|
|
504
|
+
return (text, { tone }): ReactNode =>
|
|
505
|
+
text.trim() ? (
|
|
506
|
+
<TrimmedMarkdown text={text} tone={tone} onLinkPress={onLinkPress} onCopy={options.onCopy} />
|
|
507
|
+
) : (
|
|
508
|
+
<Text />
|
|
509
|
+
);
|
|
510
|
+
}
|