@retor/react-native 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,147 +1,207 @@
1
1
  # @retor/react-native
2
2
 
3
- Embed Retor 3D experiences in a React Native / Expo app via `react-native-webview`.
3
+ Embed Retor 3D experiences in a React Native / Expo app with composable bottom-sheet UI built on `@gorhom/bottom-sheet`.
4
4
 
5
5
  ## Installation
6
6
 
7
- Install the peer dependency:
8
-
9
7
  ```bash
10
8
  # Expo
11
- npx expo install react-native-webview
9
+ npx expo install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg
10
+ npm install @gorhom/bottom-sheet lucide-react-native @retor/react-native
12
11
 
13
12
  # bare React Native
14
- npm install react-native-webview
13
+ npm install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg @gorhom/bottom-sheet lucide-react-native @retor/react-native
15
14
  cd ios && pod install
16
15
  ```
17
16
 
18
- Then install the SDK:
17
+ You also need to wrap your app root in a `GestureHandlerRootView` (per `@gorhom/bottom-sheet` requirements):
19
18
 
20
- ```bash
21
- npm install @retor/react-native
22
- # or
23
- pnpm add @retor/react-native
24
- # or
25
- yarn add @retor/react-native
19
+ ```tsx
20
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
21
+
22
+ export default function App() {
23
+ return (
24
+ <GestureHandlerRootView style={{ flex: 1 }}>
25
+ <YourApp />
26
+ </GestureHandlerRootView>
27
+ );
28
+ }
26
29
  ```
27
30
 
28
- ## Quick Start
31
+ ## Quick Start — Default UI
32
+
33
+ The fastest way: drop in `<Hud>` with the three sheets and they'll work out of the box.
29
34
 
30
35
  ```tsx
31
36
  import { View } from "react-native";
32
- import { Viewer, Hud } from "@retor/react-native";
37
+ import { Viewer, Hud, ProjectSheet, LineDetailSheet, AddNoteSheet } from "@retor/react-native";
33
38
 
34
39
  export default function Scene() {
35
40
  return (
36
41
  <View style={{ flex: 1 }}>
37
- <Viewer projectId="abc123" style={{ flex: 1 }}>
38
- <Hud />
42
+ <Viewer projectId="abc123">
43
+ <Hud>
44
+ <ProjectSheet />
45
+ <LineDetailSheet />
46
+ <AddNoteSheet />
47
+ </Hud>
39
48
  </Viewer>
40
49
  </View>
41
50
  );
42
51
  }
43
52
  ```
44
53
 
45
- ## Custom Bottom Sheet
54
+ What you get:
55
+ - A bottom sheet showing the project name and a horizontal carousel of lines
56
+ - Tap a line → second sheet opens with the tag list and a Done button
57
+ - Tap a tag → camera scrolls to it
58
+ - Done returns to the browse sheet
46
59
 
47
- Listen to events and drive a native UI on top of the viewer. Use the `useViewer` hook from any component to control the viewer without threading refs around.
60
+ ## Concepts
48
61
 
49
- ```tsx
50
- import { useState } from "react";
51
- import { View, Text, Pressable, FlatList } from "react-native";
52
- import { Viewer, useViewer, type RetorLine } from "@retor/react-native";
62
+ - **`<Viewer>`** wraps the WebView and exposes scene state via React context. Always vanilla — Retor itself shows no UI.
63
+ - **`<Hud>`** sets up `BottomSheetModalProvider`. Place sheets and other overlays inside.
64
+ - **Sheets** auto-present based on bridge state:
65
+ - `<ProjectSheet>` shows when no line is open
66
+ - `<LineDetailSheet>` shows when a line is open
67
+ - `<AddNoteSheet>` shows when `useAddNote().open()` is called
68
+ - **Composition components** (`<LinesCarousel>`, `<LineTagList>`) take a render prop so you can replace the visuals while keeping the data wiring.
69
+
70
+ ## Customising the visuals
53
71
 
54
- function Sheet({ lines, activeLine, closestTagId }: {
55
- lines: RetorLine[];
56
- activeLine: RetorLine | null;
57
- closestTagId: string | null;
58
- }) {
59
- const { openLine, exitLine, scrollToTag } = useViewer();
72
+ Each sheet accepts:
73
+ - `snapPoints` — override the default snap points
74
+ - `renderHeader` replace the header
75
+ - `children` replace the body (typically a render-prop list)
60
76
 
77
+ ```tsx
78
+ import { Pressable, Text } from "react-native";
79
+ import { ProjectSheet, LinesCarousel, LineDetailSheet, LineTagList, useViewer } from "@retor/react-native";
80
+
81
+ function MyLineCard({ line }: { line: RetorLine }) {
82
+ const { openLine } = useViewer();
61
83
  return (
62
- <View style={{ position: "absolute", bottom: 32, left: 16, right: 16 }}>
63
- {activeLine ? (
64
- <View style={{ backgroundColor: "rgba(0,0,0,0.85)", borderRadius: 24, padding: 16 }}>
65
- <Text style={{ color: "white", fontSize: 18, fontWeight: "600" }}>{activeLine.name}</Text>
66
- <FlatList
67
- data={activeLine.tags}
68
- keyExtractor={(t) => t._id}
69
- renderItem={({ item }) => (
70
- <Pressable onPress={() => scrollToTag(item._id)}>
71
- <Text style={{ color: item._id === closestTagId ? "white" : "gray" }}>
72
- {item.name}
73
- </Text>
74
- </Pressable>
75
- )}
76
- />
77
- <Pressable onPress={() => exitLine()}>
78
- <Text style={{ color: "white" }}>Done</Text>
79
- </Pressable>
80
- </View>
81
- ) : (
82
- <FlatList
83
- horizontal
84
- data={lines}
85
- keyExtractor={(l) => l._id}
86
- renderItem={({ item }) => (
87
- <Pressable onPress={() => openLine(item._id)}>
88
- <Text style={{ color: "white", padding: 16 }}>{item.name}</Text>
89
- </Pressable>
90
- )}
91
- />
92
- )}
93
- </View>
84
+ <Pressable
85
+ onPress={() => openLine(line._id)}
86
+ style={{ width: 200, padding: 16, backgroundColor: "#222", borderRadius: 16 }}
87
+ >
88
+ <Text style={{ color: "white", fontWeight: "600" }}>{line.name}</Text>
89
+ </Pressable>
94
90
  );
95
91
  }
96
92
 
93
+ <ProjectSheet snapPoints={["20%", "50%"]}>
94
+ <LinesCarousel>
95
+ {(line) => <MyLineCard line={line} />}
96
+ </LinesCarousel>
97
+ </ProjectSheet>
98
+
99
+ <LineDetailSheet>
100
+ <LineTagList>
101
+ {(tag, isActive) => (
102
+ <Pressable style={{ padding: 12 }}>
103
+ <Text style={{ color: isActive ? "white" : "gray" }}>{tag.name}</Text>
104
+ </Pressable>
105
+ )}
106
+ </LineTagList>
107
+ </LineDetailSheet>
108
+ ```
109
+
110
+ ## Notes
111
+
112
+ `<AddNoteSheet>` triggers when you call `useAddNote().open(tagId?)` — for example from a "+" button on the active tag in your `LineTagList`. It collects text + private/public, then calls `onNoteSubmit` on the parent `<Viewer>`. **Persistence is your responsibility** — store the note however you like, then re-pass updated notes back to Retor via `<Notes>`.
113
+
114
+ ```tsx
115
+ import { useState } from "react";
116
+ import { Viewer, Hud, ProjectSheet, LineDetailSheet, AddNoteSheet, Notes, useAddNote, type RetorTag } from "@retor/react-native";
117
+
118
+ function AddButton() {
119
+ const { open } = useAddNote();
120
+ return <Pressable onPress={() => open()}><Text>+</Text></Pressable>;
121
+ }
122
+
97
123
  export default function Scene() {
98
- const [lines, setLines] = useState<RetorLine[]>([]);
99
- const [activeLine, setActiveLine] = useState<RetorLine | null>(null);
100
- const [closestTagId, setClosestTagId] = useState<string | null>(null);
124
+ const [notes, setNotes] = useState<RetorTag[]>([]);
101
125
 
102
126
  return (
103
- <View style={{ flex: 1 }}>
104
- <Viewer
105
- projectId="abc123"
106
- style={{ flex: 1 }}
107
- onInit={(data) => setLines(data.lines)}
108
- onLineOpen={({ lineId }) => {
109
- const line = lines.find((l) => l._id === lineId);
110
- if (line) setActiveLine(line);
111
- }}
112
- onLineClose={() => setActiveLine(null)}
113
- onLineProgress={({ closestTagId }) => setClosestTagId(closestTagId)}
114
- />
115
- <Sheet lines={lines} activeLine={activeLine} closestTagId={closestTagId} />
116
- </View>
127
+ <Viewer
128
+ projectId="abc123"
129
+ onNoteSubmit={({ text, isPrivate, tagId, lineId, position }) => {
130
+ if (!position) return;
131
+ setNotes((prev) => [
132
+ ...prev,
133
+ {
134
+ _id: `note-${Date.now()}`,
135
+ name: text,
136
+ position,
137
+ objectId: lineId ?? undefined,
138
+ },
139
+ ]);
140
+ }}
141
+ >
142
+ <Notes notes={notes} />
143
+ <Hud>
144
+ <ProjectSheet />
145
+ <LineDetailSheet>
146
+ <LineTagList>
147
+ {(tag, isActive) => (
148
+ <View style={{ flexDirection: "row", padding: 12 }}>
149
+ <Text style={{ flex: 1, color: isActive ? "white" : "gray" }}>{tag.name}</Text>
150
+ {isActive && <AddButton />}
151
+ </View>
152
+ )}
153
+ </LineTagList>
154
+ </LineDetailSheet>
155
+ <AddNoteSheet />
156
+ </Hud>
157
+ </Viewer>
117
158
  );
118
159
  }
119
160
  ```
120
161
 
121
- ## Notes on WebView setup
162
+ ## Hooks
122
163
 
123
- - The SDK sets `originWhitelist=["*"]`, `javaScriptEnabled`, and `domStorageEnabled` automatically
124
- - Inline media playback is enabled without user gesture
125
- - Messages are JSON-stringified across the bridge — handled transparently
126
- - On Android, inbound messages are dispatched via `injectJavaScript`
164
+ All hooks read from the bridge context provided by the parent `<Viewer>`.
127
165
 
128
- ## Passing in Notes
166
+ | Hook | Returns |
167
+ |------|---------|
168
+ | `useProject()` | The current `RetorProject` (or null) |
169
+ | `useLines()` | Array of `RetorLine` |
170
+ | `useActiveLine()` | The currently open line (or null) |
171
+ | `useLineProgress()` | `{ progress, closestTagId }` |
172
+ | `useAutoplay()` | `{ isPlaying, toggle, play, pause }` |
173
+ | `useAddNote()` | `{ isOpen, tagId, open, close, submit }` |
174
+ | `useViewer()` | Imperative controls (`openLine`, `exitLine`, `scrollToTag`, ...) |
129
175
 
130
- Use `<Notes>` as a child of the Viewer to push user-generated tags into the 3D scene. Same API as the React SDK — re-render with an updated array to sync changes.
176
+ ## Imperative API
177
+
178
+ The `useViewer` hook also supports controlling a specific viewer by ID:
131
179
 
132
180
  ```tsx
133
- import { Viewer, Notes, type RetorTag } from "@retor/react-native";
181
+ <Viewer id="left" projectId="..." />
182
+ <Viewer id="right" projectId="..." />
183
+
184
+ const left = useViewer("left");
185
+ left.openLine("line-a");
186
+ ```
134
187
 
135
- const [notes, setNotes] = useState<RetorTag[]>([]);
188
+ Or pass a ref directly:
136
189
 
137
- <Viewer projectId="..." style={{ flex: 1 }}>
138
- <Notes notes={notes} />
139
- </Viewer>
190
+ ```tsx
191
+ const ref = useRef<ViewerHandle>(null);
192
+ <Viewer ref={ref} projectId="..." />
193
+ ref.current?.openLine("...");
140
194
  ```
141
195
 
142
- ## API
196
+ ## Cover photo
143
197
 
144
- Identical to the [React SDK](https://www.npmjs.com/package/@retor/react) same props, same imperative methods, same `useViewer` hook.
198
+ A static thumbnail of a project's start view no 3D, no bridge:
199
+
200
+ ```tsx
201
+ import { CoverPhoto } from "@retor/react-native";
202
+
203
+ <CoverPhoto projectId="abc123" style={{ width: 200, height: 120 }} />
204
+ ```
145
205
 
146
206
  ## License
147
207
 
package/dist/index.d.mts CHANGED
@@ -16,6 +16,7 @@ interface RetorTag {
16
16
  color?: string;
17
17
  tagType?: string;
18
18
  iconName?: string;
19
+ objectId?: string;
19
20
  }
20
21
  interface RetorLine {
21
22
  _id: string;
@@ -50,12 +51,106 @@ interface ViewerHandle {
50
51
  toggleAutoplay: () => void;
51
52
  setAutoplay: (playing: boolean) => void;
52
53
  }
54
+
55
+ interface NoteSubmitPayload {
56
+ text: string;
57
+ isPrivate: boolean;
58
+ tagId: string | null;
59
+ lineId: string | null;
60
+ position: {
61
+ x: number;
62
+ y: number;
63
+ z: number;
64
+ } | null;
65
+ }
66
+ interface RetorBridgeContextValue {
67
+ project: RetorProject | null;
68
+ lines: RetorLine[];
69
+ activeLineId: string | null;
70
+ activeLine: RetorLine | null;
71
+ closestTagId: string | null;
72
+ progress: number;
73
+ isPlaying: boolean;
74
+ isAddNoteOpen: boolean;
75
+ addNoteTagId: string | null;
76
+ controls: ViewerHandle;
77
+ openAddNote: (tagId?: string) => void;
78
+ closeAddNote: () => void;
79
+ submitNote: (text: string, isPrivate?: boolean) => void;
80
+ onNoteSubmit?: (payload: NoteSubmitPayload) => void;
81
+ }
82
+ declare function useRetorBridge(): RetorBridgeContextValue;
83
+ /** Returns the array of all lines in the current project. */
84
+ declare function useLines(): RetorLine[];
85
+ /** Returns the project metadata once `init` has fired. */
86
+ declare function useProject(): RetorProject | null;
87
+ /**
88
+ * Returns the line that's currently open (after the user enters a scroll path),
89
+ * or null when in browse mode.
90
+ */
91
+ declare function useActiveLine(): RetorLine | null;
92
+ /**
93
+ * Returns scroll progress (0..1) and the id of the closest tag along the
94
+ * currently active line. Both are null when no line is open.
95
+ */
96
+ declare function useLineProgress(): {
97
+ progress: number;
98
+ closestTagId: string | null;
99
+ };
100
+ /**
101
+ * Returns the autoplay state and controls.
102
+ */
103
+ declare function useAutoplay(): {
104
+ isPlaying: boolean;
105
+ toggle: () => void;
106
+ play: () => void;
107
+ pause: () => void;
108
+ };
109
+ /**
110
+ * Returns the current add-note flow state and actions.
111
+ */
112
+ declare function useAddNote(): {
113
+ isOpen: boolean;
114
+ tagId: string | null;
115
+ open: (tagId?: string) => void;
116
+ close: () => void;
117
+ submit: (text: string, isPrivate?: boolean) => void;
118
+ };
119
+
120
+ /**
121
+ * Hook for controlling a Viewer from anywhere in your tree.
122
+ *
123
+ * Default usage (single viewer):
124
+ * ```tsx
125
+ * const { openLine, exitLine } = useViewer();
126
+ * ```
127
+ *
128
+ * Multiple viewers:
129
+ * ```tsx
130
+ * <Viewer id="left" projectId="..." />
131
+ * const left = useViewer("left");
132
+ * ```
133
+ *
134
+ * With a ref:
135
+ * ```tsx
136
+ * const ref = useRef<ViewerHandle>(null);
137
+ * const { openLine } = useViewer(ref);
138
+ * ```
139
+ */
140
+ declare function useViewer(target?: string | React.RefObject<ViewerHandle | null>): ViewerHandle;
141
+ interface NotesProps {
142
+ /** Array of notes (same shape as RetorTag) to push into the 3D scene. */
143
+ notes: RetorTag[];
144
+ }
145
+ declare function Notes(_props: NotesProps): React.ReactElement | null;
53
146
  interface ViewerProps {
54
147
  projectId: string;
55
148
  /** Identifier for this viewer instance. Used by `useViewer(id)`. Defaults to "default". */
56
149
  id?: string;
57
150
  /** Base URL where Retor is hosted. Defaults to https://retor.app */
58
151
  baseUrl?: string;
152
+ /** Called when a note is submitted via `<AddNoteSheet>`. Receives the text + position. */
153
+ onNoteSubmit?: (note: NoteSubmitPayload) => void;
59
154
  onInit?: (data: InitPayload) => void;
60
155
  onLineOpen?: (data: {
61
156
  lineId: string;
@@ -66,48 +161,104 @@ interface ViewerProps {
66
161
  style?: StyleProp<ViewStyle>;
67
162
  children?: React.ReactNode;
68
163
  }
164
+ declare const Viewer: React.ForwardRefExoticComponent<ViewerProps & React.RefAttributes<ViewerHandle>>;
165
+
166
+ interface HudProps {
167
+ children: React.ReactNode;
168
+ }
69
169
  /**
70
- * Hook that returns a destructured set of viewer controls.
170
+ * Sets up the bottom-sheet provider tree for the SDK's UI components
171
+ * (`<ProjectSheet>`, `<LineDetailSheet>`, `<AddNoteSheet>`).
71
172
  *
72
- * Usage with an ID (default "default"):
73
- * ```tsx
74
- * <Viewer projectId="..." /> // id defaults to "default"
75
- * const { openLine, exitLine } = useViewer();
76
- * ```
77
- *
78
- * With an explicit ID (multiple viewers on one screen):
79
- * ```tsx
80
- * <Viewer id="left" projectId="..." />
81
- * <Viewer id="right" projectId="..." />
82
- * const left = useViewer("left");
83
- * left.openLine("...");
84
- * ```
173
+ * Place inside `<Viewer>` so the sheets can read scene state from the bridge:
85
174
  *
86
- * With a ref (if you prefer imperative style):
87
175
  * ```tsx
88
- * const ref = useRef<ViewerHandle>(null);
89
- * <Viewer ref={ref} projectId="..." />
90
- * const { openLine } = useViewer(ref);
176
+ * <Viewer projectId="...">
177
+ * <Hud>
178
+ * <ProjectSheet />
179
+ * <LineDetailSheet />
180
+ * <AddNoteSheet />
181
+ * </Hud>
182
+ * </Viewer>
91
183
  * ```
92
184
  */
93
- declare function useViewer(target?: string | React.RefObject<ViewerHandle | null>): ViewerHandle;
185
+ declare function Hud({ children }: HudProps): React.JSX.Element;
186
+
187
+ interface ProjectSheetProps {
188
+ /** Snap points. Defaults to ["35%", "85%"]. */
189
+ snapPoints?: (string | number)[];
190
+ /** Override the default header (project name + description + arrow button). */
191
+ renderHeader?: (project: {
192
+ name?: string;
193
+ description?: string;
194
+ }) => React.ReactNode;
195
+ /** Children to render inside. Defaults to a built-in `<LinesCarousel>`. */
196
+ children?: React.ReactNode;
197
+ }
94
198
  /**
95
- * Include as a child of `<Viewer>` to show Retor's built-in UI
96
- * (info card, tag list, autoplay controls). Without it, the viewer runs in
97
- * vanilla mode — you build your own UI around the 3D scene using the props.
199
+ * Browse-mode bottom sheet auto-presents whenever no line is open.
98
200
  */
99
- declare function Hud(): React.ReactElement | null;
100
- interface NotesProps {
101
- /** Array of notes (same shape as RetorTag) to pass into the 3D scene. */
102
- notes: RetorTag[];
201
+ declare function ProjectSheet({ snapPoints, renderHeader, children }: ProjectSheetProps): React.JSX.Element;
202
+ interface LinesCarouselProps {
203
+ children: (line: RetorLine, index: number) => React.ReactNode;
204
+ gap?: number;
205
+ paddingHorizontal?: number;
206
+ }
207
+ declare function LinesCarousel({ children, gap, paddingHorizontal }: LinesCarouselProps): React.JSX.Element | null;
208
+
209
+ interface LineDetailSheetProps {
210
+ /** Snap points. Defaults to ["35%", "85%"]. */
211
+ snapPoints?: (string | number)[];
212
+ /** Override the header. */
213
+ renderHeader?: (line: RetorLine) => React.ReactNode;
214
+ /** Custom content (typically a `<LineTagList>`). */
215
+ children?: React.ReactNode;
103
216
  }
104
217
  /**
105
- * Pass user-generated notes into the Retor scene as a child of `<Viewer>`.
106
- * The notes use the same shape as a RetorTag — Retor will render them in
107
- * the scene and tag lists. Persistence is handled entirely by your app:
108
- * re-render with an updated `notes` array to sync changes.
218
+ * Sheet shown when a line is open. Includes a default header with the
219
+ * autoplay button + minimize arrow, the tag list, and a Done footer.
109
220
  */
110
- declare function Notes(_props: NotesProps): React.ReactElement | null;
111
- declare const Viewer: React.ForwardRefExoticComponent<ViewerProps & React.RefAttributes<ViewerHandle>>;
221
+ declare function LineDetailSheet({ snapPoints, renderHeader, children }: LineDetailSheetProps): React.JSX.Element;
222
+ interface LineTagListProps {
223
+ children: (tag: RetorTag, isActive: boolean) => React.ReactNode;
224
+ }
225
+ declare function LineTagList({ children }: LineTagListProps): React.JSX.Element | null;
226
+
227
+ interface AddNoteSheetProps {
228
+ /** Snap points. Defaults to ["50%"]. */
229
+ snapPoints?: (string | number)[];
230
+ /** Max characters allowed. Defaults to 280. */
231
+ maxLength?: number;
232
+ /** Placeholder text. */
233
+ placeholder?: string;
234
+ /** Override the entire form. Receives helpers + state. */
235
+ renderForm?: (api: AddNoteFormApi) => React.ReactNode;
236
+ }
237
+ interface AddNoteFormApi {
238
+ text: string;
239
+ setText: (v: string) => void;
240
+ isPrivate: boolean;
241
+ setPrivate: (v: boolean) => void;
242
+ submit: () => void;
243
+ close: () => void;
244
+ maxLength: number;
245
+ }
246
+ /**
247
+ * Sheet for adding a note. Auto-presents when `useAddNote().open()` is called.
248
+ * On submit, fires `onNoteSubmit` on the parent `<Viewer>` with the text + position.
249
+ */
250
+ declare function AddNoteSheet({ snapPoints, maxLength, placeholder, renderForm, }: AddNoteSheetProps): React.JSX.Element;
251
+
252
+ interface CoverPhotoProps {
253
+ projectId: string;
254
+ /** Base URL where Retor is hosted. Defaults to https://retor.app */
255
+ baseUrl?: string;
256
+ style?: StyleProp<ViewStyle>;
257
+ }
258
+ /**
259
+ * Renders a Retor project's start view as a static cover image.
260
+ * No 3D rendering, no bridge — use as a thumbnail or hero.
261
+ */
262
+ declare function CoverPhoto({ projectId, baseUrl, style }: CoverPhotoProps): React.JSX.Element;
112
263
 
113
- export { Hud, type InitPayload, type LineProgressPayload, Notes, type NotesProps, type RetorLine, type RetorProject, type RetorTag, Viewer, type ViewerHandle, type ViewerProps, useViewer };
264
+ export { type AddNoteFormApi, AddNoteSheet, type AddNoteSheetProps, CoverPhoto, type CoverPhotoProps, Hud, type HudProps, type InitPayload, LineDetailSheet, type LineDetailSheetProps, type LineProgressPayload, LineTagList, type LineTagListProps, LinesCarousel, type LinesCarouselProps, type NoteSubmitPayload, Notes, type NotesProps, ProjectSheet, type ProjectSheetProps, type RetorBridgeContextValue, type RetorLine, type RetorProject, type RetorTag, Viewer, type ViewerHandle, type ViewerProps, useActiveLine, useAddNote, useAutoplay, useLineProgress, useLines, useProject, useRetorBridge, useViewer };