@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/dist/index.js CHANGED
@@ -30,13 +30,95 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.tsx
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AddNoteSheet: () => AddNoteSheet,
34
+ CoverPhoto: () => CoverPhoto,
33
35
  Hud: () => Hud,
36
+ LineDetailSheet: () => LineDetailSheet,
37
+ LineTagList: () => LineTagList,
38
+ LinesCarousel: () => LinesCarousel,
34
39
  Notes: () => Notes,
40
+ ProjectSheet: () => ProjectSheet,
35
41
  Viewer: () => Viewer,
42
+ useActiveLine: () => useActiveLine,
43
+ useAddNote: () => useAddNote,
44
+ useAutoplay: () => useAutoplay,
45
+ useLineProgress: () => useLineProgress,
46
+ useLines: () => useLines,
47
+ useProject: () => useProject,
48
+ useRetorBridge: () => useRetorBridge,
36
49
  useViewer: () => useViewer
37
50
  });
38
51
  module.exports = __toCommonJS(index_exports);
52
+
53
+ // src/context.tsx
39
54
  var import_react = __toESM(require("react"));
55
+ var RetorBridgeContext = (0, import_react.createContext)(null);
56
+ function noop() {
57
+ }
58
+ var noopHandle = {
59
+ openLine: noop,
60
+ exitLine: noop,
61
+ scrollToTag: noop,
62
+ toggleAutoplay: noop,
63
+ setAutoplay: noop
64
+ };
65
+ var fallback = {
66
+ project: null,
67
+ lines: [],
68
+ activeLineId: null,
69
+ activeLine: null,
70
+ closestTagId: null,
71
+ progress: 0,
72
+ isPlaying: false,
73
+ isAddNoteOpen: false,
74
+ addNoteTagId: null,
75
+ controls: noopHandle,
76
+ openAddNote: noop,
77
+ closeAddNote: noop,
78
+ submitNote: noop
79
+ };
80
+ function useRetorBridge() {
81
+ return (0, import_react.useContext)(RetorBridgeContext) ?? fallback;
82
+ }
83
+ function useLines() {
84
+ return useRetorBridge().lines;
85
+ }
86
+ function useProject() {
87
+ return useRetorBridge().project;
88
+ }
89
+ function useActiveLine() {
90
+ return useRetorBridge().activeLine;
91
+ }
92
+ function useLineProgress() {
93
+ const { progress, closestTagId } = useRetorBridge();
94
+ return (0, import_react.useMemo)(() => ({ progress, closestTagId }), [progress, closestTagId]);
95
+ }
96
+ function useAutoplay() {
97
+ const { isPlaying, controls } = useRetorBridge();
98
+ return (0, import_react.useMemo)(() => ({
99
+ isPlaying,
100
+ toggle: () => controls.toggleAutoplay(),
101
+ play: () => controls.setAutoplay(true),
102
+ pause: () => controls.setAutoplay(false)
103
+ }), [isPlaying, controls]);
104
+ }
105
+ function useAddNote() {
106
+ const { isAddNoteOpen, addNoteTagId, openAddNote, closeAddNote, submitNote } = useRetorBridge();
107
+ return (0, import_react.useMemo)(() => ({
108
+ isOpen: isAddNoteOpen,
109
+ tagId: addNoteTagId,
110
+ open: openAddNote,
111
+ close: closeAddNote,
112
+ submit: submitNote
113
+ }), [isAddNoteOpen, addNoteTagId, openAddNote, closeAddNote, submitNote]);
114
+ }
115
+ function RetorBridgeProvider({ value, children }) {
116
+ return /* @__PURE__ */ import_react.default.createElement(RetorBridgeContext.Provider, { value }, children);
117
+ }
118
+
119
+ // src/Viewer.tsx
120
+ var import_react2 = __toESM(require("react"));
121
+ var import_react_native = require("react-native");
40
122
  var import_react_native_webview = require("react-native-webview");
41
123
  var viewerRegistry = /* @__PURE__ */ new Map();
42
124
  var registryListeners = /* @__PURE__ */ new Set();
@@ -55,17 +137,17 @@ function subscribeRegistry(cb) {
55
137
  };
56
138
  }
57
139
  function useViewer(target = "default") {
58
- const subscribe = (0, import_react.useCallback)(
140
+ const subscribe = (0, import_react2.useCallback)(
59
141
  (cb) => typeof target === "string" ? subscribeRegistry(cb) : () => {
60
142
  },
61
143
  [target]
62
144
  );
63
- const getSnapshot = (0, import_react.useCallback)(
145
+ const getSnapshot = (0, import_react2.useCallback)(
64
146
  () => typeof target === "string" ? viewerRegistry.get(target) ?? null : target.current ?? null,
65
147
  [target]
66
148
  );
67
- const handle = (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
68
- return (0, import_react.useMemo)(() => {
149
+ const handle = (0, import_react2.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
150
+ return (0, import_react2.useMemo)(() => {
69
151
  const resolve = () => typeof target === "string" ? viewerRegistry.get(target) ?? null : target.current ?? null;
70
152
  return {
71
153
  openLine: (lineId) => {
@@ -86,27 +168,14 @@ function useViewer(target = "default") {
86
168
  };
87
169
  }, [target, handle]);
88
170
  }
89
- function Hud() {
90
- return null;
91
- }
92
- Hud.__isHud = true;
93
- function hasHud(children) {
94
- let found = false;
95
- import_react.default.Children.forEach(children, (child) => {
96
- if (!import_react.default.isValidElement(child)) return;
97
- const type = child.type;
98
- if (type?.__isHud) found = true;
99
- });
100
- return found;
101
- }
102
171
  function Notes(_props) {
103
172
  return null;
104
173
  }
105
174
  Notes.__isNotes = true;
106
175
  function extractNotes(children) {
107
176
  let notes = null;
108
- import_react.default.Children.forEach(children, (child) => {
109
- if (!import_react.default.isValidElement(child)) return;
177
+ import_react2.default.Children.forEach(children, (child) => {
178
+ if (!import_react2.default.isValidElement(child)) return;
110
179
  const type = child.type;
111
180
  if (type?.__isNotes) {
112
181
  const props = child.props;
@@ -115,54 +184,83 @@ function extractNotes(children) {
115
184
  });
116
185
  return notes;
117
186
  }
118
- var Viewer = (0, import_react.forwardRef)(
119
- function Viewer2({ projectId, id = "default", baseUrl = "https://retor.app", onInit, onLineOpen, onLineClose, onLineProgress, onMessage, style, children }, ref) {
120
- const webviewRef = (0, import_react.useRef)(null);
121
- const showHud = hasHud(children);
122
- const notes = extractNotes(children);
123
- const readyRef = (0, import_react.useRef)(false);
124
- (0, import_react.useEffect)(() => {
125
- const handle = {
126
- openLine: (lineId) => send("open-line", { lineId }),
127
- exitLine: () => send("exit-line"),
128
- scrollToTag: (tagId) => send("scroll-to-tag", { tagId }),
129
- toggleAutoplay: () => send("toggle-autoplay"),
130
- setAutoplay: (playing) => send("set-autoplay", { playing })
131
- };
132
- return registerViewer(id, handle);
133
- }, [id]);
134
- const uri = (0, import_react.useMemo)(() => {
135
- const params = [];
136
- if (!showHud) params.push("vanilla=true");
137
- const qs = params.length ? `?${params.join("&")}` : "";
138
- return `${baseUrl}/p/${projectId}${qs}`;
139
- }, [baseUrl, projectId, showHud]);
140
- const send = (0, import_react.useCallback)((type, payload) => {
141
- const message = JSON.stringify({ source: "retor-host", type, payload });
142
- const escaped = message.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
143
- const script = `
144
- (function(){
145
- try {
146
- var msg = JSON.parse(\`${escaped}\`);
147
- window.dispatchEvent(new MessageEvent('message', { data: msg }));
148
- } catch(e) {}
149
- true;
150
- })();
151
- `;
152
- webviewRef.current?.injectJavaScript(script);
153
- }, []);
154
- (0, import_react.useImperativeHandle)(ref, () => ({
187
+ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "default", baseUrl = "https://retor.app", onNoteSubmit, onInit, onLineOpen, onLineClose, onLineProgress, onMessage, style, children }, ref) {
188
+ const webviewRef = (0, import_react2.useRef)(null);
189
+ const notes = extractNotes(children);
190
+ const readyRef = (0, import_react2.useRef)(false);
191
+ const [project, setProject] = (0, import_react2.useState)(null);
192
+ const [lines, setLines] = (0, import_react2.useState)([]);
193
+ const [activeLineId, setActiveLineId] = (0, import_react2.useState)(null);
194
+ const [closestTagId, setClosestTagId] = (0, import_react2.useState)(null);
195
+ const [progress, setProgress] = (0, import_react2.useState)(0);
196
+ const [isPlaying, setIsPlaying] = (0, import_react2.useState)(false);
197
+ const [isAddNoteOpen, setIsAddNoteOpen] = (0, import_react2.useState)(false);
198
+ const [addNoteTagId, setAddNoteTagId] = (0, import_react2.useState)(null);
199
+ const uri = (0, import_react2.useMemo)(() => `${baseUrl}/p/${projectId}?vanilla=true`, [baseUrl, projectId]);
200
+ const send = (0, import_react2.useCallback)((type, payload) => {
201
+ const message = JSON.stringify({ source: "retor-host", type, payload });
202
+ const escaped = message.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
203
+ const script = `
204
+ (function(){
205
+ try {
206
+ var msg = JSON.parse(\`${escaped}\`);
207
+ window.dispatchEvent(new MessageEvent('message', { data: msg }));
208
+ } catch(e) {}
209
+ true;
210
+ })();
211
+ `;
212
+ webviewRef.current?.injectJavaScript(script);
213
+ }, []);
214
+ const controls = (0, import_react2.useMemo)(
215
+ () => ({
155
216
  openLine: (lineId) => send("open-line", { lineId }),
156
217
  exitLine: () => send("exit-line"),
157
218
  scrollToTag: (tagId) => send("scroll-to-tag", { tagId }),
158
- toggleAutoplay: () => send("toggle-autoplay"),
159
- setAutoplay: (playing) => send("set-autoplay", { playing })
160
- }), [send]);
161
- (0, import_react.useEffect)(() => {
162
- if (!notes || !readyRef.current) return;
163
- send("set-notes", { notes });
164
- }, [notes, send]);
165
- const handleMessage = (0, import_react.useCallback)((event) => {
219
+ toggleAutoplay: () => {
220
+ setIsPlaying((v) => !v);
221
+ send("toggle-autoplay");
222
+ },
223
+ setAutoplay: (playing) => {
224
+ setIsPlaying(playing);
225
+ send("set-autoplay", { playing });
226
+ }
227
+ }),
228
+ [send]
229
+ );
230
+ (0, import_react2.useImperativeHandle)(ref, () => controls, [controls]);
231
+ (0, import_react2.useEffect)(() => {
232
+ return registerViewer(id, controls);
233
+ }, [id, controls]);
234
+ (0, import_react2.useEffect)(() => {
235
+ if (!notes || !readyRef.current) return;
236
+ send("set-notes", { notes });
237
+ }, [notes, send]);
238
+ const openAddNote = (0, import_react2.useCallback)((tagId) => {
239
+ setAddNoteTagId(tagId ?? closestTagId ?? null);
240
+ setIsAddNoteOpen(true);
241
+ }, [closestTagId]);
242
+ const closeAddNote = (0, import_react2.useCallback)(() => {
243
+ setIsAddNoteOpen(false);
244
+ }, []);
245
+ const submitNote = (0, import_react2.useCallback)(
246
+ (text, isPrivate = true) => {
247
+ const tagId = addNoteTagId;
248
+ const lineId = activeLineId;
249
+ const tag = lines.find((l) => l._id === lineId)?.tags.find((t) => t._id === tagId);
250
+ const payload = {
251
+ text,
252
+ isPrivate,
253
+ tagId,
254
+ lineId,
255
+ position: tag?.position ?? null
256
+ };
257
+ onNoteSubmit?.(payload);
258
+ setIsAddNoteOpen(false);
259
+ },
260
+ [addNoteTagId, activeLineId, lines, onNoteSubmit]
261
+ );
262
+ const handleMessage = (0, import_react2.useCallback)(
263
+ (event) => {
166
264
  let data = null;
167
265
  try {
168
266
  data = JSON.parse(event.nativeEvent.data);
@@ -171,46 +269,601 @@ var Viewer = (0, import_react.forwardRef)(
171
269
  }
172
270
  if (!data || data.source !== "retor" || !data.type) return;
173
271
  switch (data.type) {
174
- case "init":
272
+ case "init": {
273
+ const payload = data.payload;
274
+ setProject(payload.project);
275
+ setLines(payload.lines);
175
276
  if (!readyRef.current) {
176
277
  readyRef.current = true;
177
278
  if (notes) send("set-notes", { notes });
178
279
  }
179
- onInit?.(data.payload);
280
+ onInit?.(payload);
180
281
  break;
181
- case "line-open":
182
- onLineOpen?.(data.payload);
282
+ }
283
+ case "line-open": {
284
+ const payload = data.payload;
285
+ setActiveLineId(payload.lineId);
286
+ onLineOpen?.(payload);
183
287
  break;
288
+ }
184
289
  case "line-close":
290
+ setActiveLineId(null);
291
+ setIsPlaying(false);
185
292
  onLineClose?.();
186
293
  break;
187
- case "line-progress":
188
- onLineProgress?.(data.payload);
294
+ case "line-progress": {
295
+ const payload = data.payload;
296
+ setProgress(payload.progress);
297
+ setClosestTagId(payload.closestTagId);
298
+ onLineProgress?.(payload);
189
299
  break;
300
+ }
190
301
  default:
191
302
  onMessage?.(data.type, data.payload);
192
303
  }
193
- }, [notes, send, onInit, onLineOpen, onLineClose, onLineProgress, onMessage]);
194
- return /* @__PURE__ */ import_react.default.createElement(
195
- import_react_native_webview.WebView,
304
+ },
305
+ [notes, send, onInit, onLineOpen, onLineClose, onLineProgress, onMessage]
306
+ );
307
+ const activeLine = (0, import_react2.useMemo)(
308
+ () => lines.find((l) => l._id === activeLineId) ?? null,
309
+ [lines, activeLineId]
310
+ );
311
+ const ctxValue = (0, import_react2.useMemo)(
312
+ () => ({
313
+ project,
314
+ lines,
315
+ activeLineId,
316
+ activeLine,
317
+ closestTagId,
318
+ progress,
319
+ isPlaying,
320
+ isAddNoteOpen,
321
+ addNoteTagId,
322
+ controls,
323
+ openAddNote,
324
+ closeAddNote,
325
+ submitNote,
326
+ onNoteSubmit
327
+ }),
328
+ [project, lines, activeLineId, activeLine, closestTagId, progress, isPlaying, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote, onNoteSubmit]
329
+ );
330
+ return /* @__PURE__ */ import_react2.default.createElement(RetorBridgeProvider, { value: ctxValue }, /* @__PURE__ */ import_react2.default.createElement(import_react_native.View, { style: [styles.root, style] }, /* @__PURE__ */ import_react2.default.createElement(
331
+ import_react_native_webview.WebView,
332
+ {
333
+ ref: webviewRef,
334
+ source: { uri },
335
+ style: styles.webview,
336
+ onMessage: handleMessage,
337
+ originWhitelist: ["*"],
338
+ javaScriptEnabled: true,
339
+ domStorageEnabled: true,
340
+ allowsInlineMediaPlayback: true,
341
+ mediaPlaybackRequiresUserAction: false
342
+ }
343
+ ), children));
344
+ });
345
+ var styles = import_react_native.StyleSheet.create({
346
+ root: { flex: 1, position: "relative" },
347
+ webview: { flex: 1 }
348
+ });
349
+
350
+ // src/Hud.tsx
351
+ var import_react3 = __toESM(require("react"));
352
+ var import_react_native2 = require("react-native");
353
+ var import_bottom_sheet = require("@gorhom/bottom-sheet");
354
+ function Hud({ children }) {
355
+ return /* @__PURE__ */ import_react3.default.createElement(import_react_native2.View, { pointerEvents: "box-none", style: import_react_native2.StyleSheet.absoluteFill }, /* @__PURE__ */ import_react3.default.createElement(import_bottom_sheet.BottomSheetModalProvider, null, children));
356
+ }
357
+
358
+ // src/ProjectSheet.tsx
359
+ var import_react4 = __toESM(require("react"));
360
+ var import_react_native3 = require("react-native");
361
+ var import_bottom_sheet2 = require("@gorhom/bottom-sheet");
362
+ var import_lucide_react_native = require("lucide-react-native");
363
+ function ProjectSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
364
+ const { project, activeLineId, isAddNoteOpen } = useRetorBridge();
365
+ const sheetRef = (0, import_react4.useRef)(null);
366
+ const snapPointsArr = (0, import_react4.useMemo)(() => snapPoints, [snapPoints]);
367
+ const [minimized, setMinimized] = (0, import_react4.useState)(false);
368
+ (0, import_react4.useEffect)(() => {
369
+ if (activeLineId || isAddNoteOpen) {
370
+ sheetRef.current?.dismiss();
371
+ } else {
372
+ sheetRef.current?.present();
373
+ setMinimized(false);
374
+ }
375
+ }, [activeLineId, isAddNoteOpen]);
376
+ const handleSheetChange = (index) => {
377
+ setMinimized(index === 0);
378
+ };
379
+ const toggleMinimize = () => {
380
+ if (minimized) {
381
+ sheetRef.current?.snapToIndex(snapPointsArr.length - 1);
382
+ } else {
383
+ sheetRef.current?.snapToIndex(0);
384
+ }
385
+ };
386
+ return /* @__PURE__ */ import_react4.default.createElement(
387
+ import_bottom_sheet2.BottomSheetModal,
388
+ {
389
+ ref: sheetRef,
390
+ snapPoints: snapPointsArr,
391
+ enablePanDownToClose: false,
392
+ enableDismissOnClose: false,
393
+ onChange: handleSheetChange,
394
+ backdropComponent: (props) => /* @__PURE__ */ import_react4.default.createElement(
395
+ import_bottom_sheet2.BottomSheetBackdrop,
396
+ {
397
+ ...props,
398
+ appearsOnIndex: 1,
399
+ disappearsOnIndex: 0,
400
+ opacity: 0.4,
401
+ pressBehavior: "collapse"
402
+ }
403
+ ),
404
+ handleIndicatorStyle: styles2.handle,
405
+ backgroundStyle: styles2.background
406
+ },
407
+ /* @__PURE__ */ import_react4.default.createElement(import_bottom_sheet2.BottomSheetView, { style: styles2.content }, renderHeader ? renderHeader({ name: project?.name, description: project?.description }) : /* @__PURE__ */ import_react4.default.createElement(import_react_native3.View, { style: styles2.header }, /* @__PURE__ */ import_react4.default.createElement(import_react_native3.View, { style: { flex: 1, minWidth: 0 } }, project?.name && /* @__PURE__ */ import_react4.default.createElement(import_react_native3.Text, { style: styles2.title, numberOfLines: 1 }, project.name), project?.description && /* @__PURE__ */ import_react4.default.createElement(import_react_native3.Text, { style: styles2.subtitle, numberOfLines: 4 }, project.description)), /* @__PURE__ */ import_react4.default.createElement(import_react_native3.Pressable, { style: styles2.iconBtn, onPress: toggleMinimize }, minimized ? /* @__PURE__ */ import_react4.default.createElement(import_lucide_react_native.ArrowUp, { size: 14, color: "rgba(255,255,255,0.6)" }) : /* @__PURE__ */ import_react4.default.createElement(import_lucide_react_native.ArrowDown, { size: 14, color: "rgba(255,255,255,0.6)" }))), children ?? /* @__PURE__ */ import_react4.default.createElement(DefaultLinesCarousel, null))
408
+ );
409
+ }
410
+ function DefaultLinesCarousel() {
411
+ return /* @__PURE__ */ import_react4.default.createElement(LinesCarousel, null, (line) => /* @__PURE__ */ import_react4.default.createElement(DefaultLineCard, { line }));
412
+ }
413
+ function LinesCarousel({ children, gap = 12, paddingHorizontal = 16 }) {
414
+ const { lines } = useRetorBridge();
415
+ if (lines.length === 0) return null;
416
+ return /* @__PURE__ */ import_react4.default.createElement(import_react_native3.View, { style: styles2.carouselWrap }, /* @__PURE__ */ import_react4.default.createElement(import_react_native3.Text, { style: styles2.carouselLabel }, "Routes"), /* @__PURE__ */ import_react4.default.createElement(
417
+ import_react_native3.ScrollView,
418
+ {
419
+ horizontal: true,
420
+ showsHorizontalScrollIndicator: false,
421
+ contentContainerStyle: { paddingHorizontal, gap }
422
+ },
423
+ lines.map((line, idx) => /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, { key: line._id }, children(line, idx)))
424
+ ));
425
+ }
426
+ function DefaultLineCard({ line }) {
427
+ const { controls } = useRetorBridge();
428
+ return /* @__PURE__ */ import_react4.default.createElement(
429
+ import_react_native3.Pressable,
430
+ {
431
+ onPress: () => controls.openLine(line._id),
432
+ style: styles2.lineCard
433
+ },
434
+ /* @__PURE__ */ import_react4.default.createElement(import_react_native3.Text, { style: styles2.lineCardTitle, numberOfLines: 1 }, line.name || "Line"),
435
+ line.subtitle && /* @__PURE__ */ import_react4.default.createElement(import_react_native3.Text, { style: styles2.lineCardSubtitle, numberOfLines: 2 }, line.subtitle)
436
+ );
437
+ }
438
+ var styles2 = import_react_native3.StyleSheet.create({
439
+ background: { backgroundColor: "rgba(20,20,20,0.95)" },
440
+ handle: { backgroundColor: "rgba(255,255,255,0.3)" },
441
+ content: { flex: 1, paddingTop: 12, paddingBottom: 24 },
442
+ header: {
443
+ flexDirection: "row",
444
+ alignItems: "flex-start",
445
+ paddingHorizontal: 24,
446
+ paddingBottom: 16,
447
+ gap: 8
448
+ },
449
+ title: { color: "white", fontSize: 18, fontWeight: "600", lineHeight: 22 },
450
+ subtitle: { color: "rgba(255,255,255,0.6)", fontSize: 13, marginTop: 4, lineHeight: 18 },
451
+ iconBtn: {
452
+ width: 28,
453
+ height: 28,
454
+ borderRadius: 14,
455
+ backgroundColor: "rgba(255,255,255,0.1)",
456
+ alignItems: "center",
457
+ justifyContent: "center"
458
+ },
459
+ carouselWrap: { marginTop: 8 },
460
+ carouselLabel: {
461
+ color: "rgba(255,255,255,0.4)",
462
+ fontSize: 10,
463
+ textTransform: "uppercase",
464
+ letterSpacing: 1,
465
+ fontWeight: "500",
466
+ paddingHorizontal: 24,
467
+ marginBottom: 8
468
+ },
469
+ lineCard: {
470
+ width: 220,
471
+ backgroundColor: "rgba(255,255,255,0.06)",
472
+ borderRadius: 16,
473
+ padding: 16
474
+ },
475
+ lineCardTitle: { color: "white", fontSize: 14, fontWeight: "600" },
476
+ lineCardSubtitle: { color: "rgba(255,255,255,0.5)", fontSize: 11, marginTop: 4 }
477
+ });
478
+
479
+ // src/LineDetailSheet.tsx
480
+ var import_react5 = __toESM(require("react"));
481
+ var import_react_native4 = require("react-native");
482
+ var import_bottom_sheet3 = require("@gorhom/bottom-sheet");
483
+ var import_react_native_svg = __toESM(require("react-native-svg"));
484
+ var import_lucide_react_native2 = require("lucide-react-native");
485
+ function LineDetailSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
486
+ const { activeLine, isAddNoteOpen, controls } = useRetorBridge();
487
+ const sheetRef = (0, import_react5.useRef)(null);
488
+ const snapPointsArr = (0, import_react5.useMemo)(() => snapPoints, [snapPoints]);
489
+ const [minimized, setMinimized] = (0, import_react5.useState)(false);
490
+ (0, import_react5.useEffect)(() => {
491
+ if (activeLine && !isAddNoteOpen) {
492
+ sheetRef.current?.present();
493
+ setMinimized(false);
494
+ } else {
495
+ sheetRef.current?.dismiss();
496
+ }
497
+ }, [activeLine, isAddNoteOpen]);
498
+ const handleSheetChange = (index) => {
499
+ setMinimized(index === 0);
500
+ };
501
+ const toggleMinimize = () => {
502
+ if (minimized) {
503
+ sheetRef.current?.snapToIndex(snapPointsArr.length - 1);
504
+ } else {
505
+ sheetRef.current?.snapToIndex(0);
506
+ }
507
+ };
508
+ return /* @__PURE__ */ import_react5.default.createElement(
509
+ import_bottom_sheet3.BottomSheetModal,
510
+ {
511
+ ref: sheetRef,
512
+ snapPoints: snapPointsArr,
513
+ enablePanDownToClose: false,
514
+ enableDismissOnClose: false,
515
+ onChange: handleSheetChange,
516
+ backdropComponent: (props) => /* @__PURE__ */ import_react5.default.createElement(
517
+ import_bottom_sheet3.BottomSheetBackdrop,
518
+ {
519
+ ...props,
520
+ appearsOnIndex: 1,
521
+ disappearsOnIndex: 0,
522
+ opacity: 0.4,
523
+ pressBehavior: "collapse"
524
+ }
525
+ ),
526
+ handleIndicatorStyle: styles3.handle,
527
+ backgroundStyle: styles3.background
528
+ },
529
+ activeLine && /* @__PURE__ */ import_react5.default.createElement(import_react_native4.View, { style: styles3.root }, renderHeader ? renderHeader(activeLine) : /* @__PURE__ */ import_react5.default.createElement(
530
+ DefaultHeader,
531
+ {
532
+ line: activeLine,
533
+ minimized,
534
+ onToggleMinimize: toggleMinimize
535
+ }
536
+ ), /* @__PURE__ */ import_react5.default.createElement(import_react_native4.View, { style: styles3.listContainer }, children ?? /* @__PURE__ */ import_react5.default.createElement(DefaultLineTagList, null)), /* @__PURE__ */ import_react5.default.createElement(import_react_native4.View, { style: styles3.footer }, /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Pressable, { style: styles3.doneButton, onPress: () => controls.exitLine() }, /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Text, { style: styles3.doneText }, "Done"))))
537
+ );
538
+ }
539
+ function DefaultHeader({
540
+ line,
541
+ minimized,
542
+ onToggleMinimize
543
+ }) {
544
+ return /* @__PURE__ */ import_react5.default.createElement(import_react_native4.View, { style: styles3.header }, /* @__PURE__ */ import_react5.default.createElement(import_react_native4.View, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Text, { style: styles3.title, numberOfLines: 1 }, line.name), line.subtitle && /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Text, { style: styles3.subtitle, numberOfLines: 1 }, line.subtitle), line.description && /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Text, { style: styles3.description, numberOfLines: minimized ? 2 : 4 }, line.description)), /* @__PURE__ */ import_react5.default.createElement(import_react_native4.View, { style: styles3.headerActions }, /* @__PURE__ */ import_react5.default.createElement(AutoplayButton, null), /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Pressable, { style: styles3.iconBtn, onPress: onToggleMinimize }, minimized ? /* @__PURE__ */ import_react5.default.createElement(import_lucide_react_native2.ArrowUp, { size: 14, color: "rgba(255,255,255,0.6)" }) : /* @__PURE__ */ import_react5.default.createElement(import_lucide_react_native2.ArrowDown, { size: 14, color: "rgba(255,255,255,0.6)" }))));
545
+ }
546
+ function AutoplayButton() {
547
+ const { isPlaying, progress, controls } = useRetorBridge();
548
+ const r = 12.5;
549
+ const c = 2 * Math.PI * r;
550
+ return /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Pressable, { style: styles3.iconBtn, onPress: () => controls.toggleAutoplay() }, /* @__PURE__ */ import_react5.default.createElement(import_react_native_svg.default, { width: 28, height: 28, style: import_react_native4.StyleSheet.absoluteFill }, /* @__PURE__ */ import_react5.default.createElement(
551
+ import_react_native_svg.Circle,
552
+ {
553
+ cx: 14,
554
+ cy: 14,
555
+ r,
556
+ fill: "none",
557
+ stroke: "white",
558
+ strokeWidth: 2,
559
+ strokeDasharray: `${c}`,
560
+ strokeDashoffset: c * (1 - progress),
561
+ strokeLinecap: "round",
562
+ transform: "rotate(-90, 14, 14)"
563
+ }
564
+ )), isPlaying ? /* @__PURE__ */ import_react5.default.createElement(import_lucide_react_native2.Pause, { size: 11, color: "white", fill: "white" }) : /* @__PURE__ */ import_react5.default.createElement(import_lucide_react_native2.Play, { size: 11, color: "white", fill: "white", style: { marginLeft: 1 } }));
565
+ }
566
+ function LineTagList({ children }) {
567
+ const { activeLine, closestTagId } = useRetorBridge();
568
+ const listRef = (0, import_react5.useRef)(null);
569
+ const tags = (0, import_react5.useMemo)(
570
+ () => (activeLine?.tags ?? []).filter((t) => t.name && t.name.trim().length > 0),
571
+ [activeLine]
572
+ );
573
+ (0, import_react5.useEffect)(() => {
574
+ if (!closestTagId) return;
575
+ const index = tags.findIndex((t) => t._id === closestTagId);
576
+ if (index < 0) return;
577
+ requestAnimationFrame(() => {
578
+ try {
579
+ listRef.current?.scrollToIndex({ index, animated: true, viewPosition: 0 });
580
+ } catch {
581
+ }
582
+ });
583
+ }, [closestTagId, tags]);
584
+ if (!activeLine) return null;
585
+ return /* @__PURE__ */ import_react5.default.createElement(
586
+ import_bottom_sheet3.BottomSheetFlatList,
587
+ {
588
+ ref: listRef,
589
+ data: tags,
590
+ keyExtractor: (t) => t._id,
591
+ renderItem: ({ item }) => /* @__PURE__ */ import_react5.default.createElement(import_react_native4.View, null, children(item, item._id === closestTagId)),
592
+ contentContainerStyle: styles3.list,
593
+ onScrollToIndexFailed: (info) => {
594
+ setTimeout(() => {
595
+ listRef.current?.scrollToOffset({ offset: info.averageItemLength * info.index, animated: true });
596
+ }, 100);
597
+ }
598
+ }
599
+ );
600
+ }
601
+ function DefaultLineTagList() {
602
+ return /* @__PURE__ */ import_react5.default.createElement(LineTagList, null, (tag, isActive) => /* @__PURE__ */ import_react5.default.createElement(DefaultTagItem, { tag, isActive }));
603
+ }
604
+ function DefaultTagItem({ tag, isActive }) {
605
+ const { controls, openAddNote, activeLine } = useRetorBridge();
606
+ const showPlus = isActive && (activeLine?.notesSupported ?? false);
607
+ return /* @__PURE__ */ import_react5.default.createElement(
608
+ import_react_native4.Pressable,
609
+ {
610
+ onPress: () => controls.scrollToTag(tag._id),
611
+ style: [styles3.tagItem, isActive && styles3.tagItemActive]
612
+ },
613
+ /* @__PURE__ */ import_react5.default.createElement(import_react_native4.View, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Text, { style: [styles3.tagText, isActive && styles3.tagTextActive], numberOfLines: 1 }, tag.name), isActive && tag.description && /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Text, { style: styles3.tagDescription, numberOfLines: 2 }, tag.description)),
614
+ tag.subtitle && /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Text, { style: styles3.tagSubtitle, numberOfLines: 1 }, tag.subtitle),
615
+ showPlus && /* @__PURE__ */ import_react5.default.createElement(
616
+ import_react_native4.Pressable,
196
617
  {
197
- ref: webviewRef,
198
- source: { uri },
199
- style,
200
- onMessage: handleMessage,
201
- originWhitelist: ["*"],
202
- javaScriptEnabled: true,
203
- domStorageEnabled: true,
204
- allowsInlineMediaPlayback: true,
205
- mediaPlaybackRequiresUserAction: false
618
+ onPress: (e) => {
619
+ e.stopPropagation();
620
+ openAddNote(tag._id);
621
+ },
622
+ style: styles3.plusBtn
623
+ },
624
+ /* @__PURE__ */ import_react5.default.createElement(import_lucide_react_native2.Plus, { size: 14, color: "white" })
625
+ )
626
+ );
627
+ }
628
+ var styles3 = import_react_native4.StyleSheet.create({
629
+ background: { backgroundColor: "rgba(20,20,20,0.95)" },
630
+ handle: { backgroundColor: "rgba(255,255,255,0.3)" },
631
+ root: { flex: 1 },
632
+ header: {
633
+ flexDirection: "row",
634
+ alignItems: "flex-start",
635
+ paddingHorizontal: 24,
636
+ paddingTop: 8,
637
+ paddingBottom: 12,
638
+ gap: 8
639
+ },
640
+ headerActions: { flexDirection: "row", alignItems: "center", gap: 6 },
641
+ title: { color: "white", fontSize: 18, fontWeight: "700" },
642
+ subtitle: { color: "rgba(255,255,255,0.5)", fontSize: 12, marginTop: 2 },
643
+ description: { color: "rgba(255,255,255,0.6)", fontSize: 12, marginTop: 6, lineHeight: 18 },
644
+ iconBtn: {
645
+ width: 28,
646
+ height: 28,
647
+ borderRadius: 14,
648
+ backgroundColor: "rgba(255,255,255,0.1)",
649
+ alignItems: "center",
650
+ justifyContent: "center",
651
+ position: "relative"
652
+ },
653
+ listContainer: { flex: 1 },
654
+ list: { paddingHorizontal: 16, paddingBottom: 8, gap: 4 },
655
+ tagItem: {
656
+ flexDirection: "row",
657
+ alignItems: "center",
658
+ paddingHorizontal: 12,
659
+ paddingVertical: 12,
660
+ borderRadius: 12,
661
+ gap: 8
662
+ },
663
+ tagItemActive: { backgroundColor: "rgba(255,255,255,0.1)" },
664
+ tagText: { color: "rgba(255,255,255,0.6)", fontSize: 13 },
665
+ tagTextActive: { color: "white", fontWeight: "600" },
666
+ tagDescription: { color: "rgba(255,255,255,0.4)", fontSize: 11, marginTop: 2, lineHeight: 14 },
667
+ tagSubtitle: { color: "rgba(255,255,255,0.4)", fontSize: 10 },
668
+ plusBtn: {
669
+ width: 22,
670
+ height: 22,
671
+ borderRadius: 11,
672
+ backgroundColor: "rgba(255,255,255,0.15)",
673
+ alignItems: "center",
674
+ justifyContent: "center"
675
+ },
676
+ footer: { paddingHorizontal: 16, paddingBottom: 16, paddingTop: 8 },
677
+ doneButton: {
678
+ backgroundColor: "white",
679
+ borderRadius: 14,
680
+ paddingVertical: 14,
681
+ alignItems: "center"
682
+ },
683
+ doneText: { color: "black", fontSize: 14, fontWeight: "600" }
684
+ });
685
+
686
+ // src/AddNoteSheet.tsx
687
+ var import_react6 = __toESM(require("react"));
688
+ var import_react_native5 = require("react-native");
689
+ var import_bottom_sheet4 = require("@gorhom/bottom-sheet");
690
+ var import_lucide_react_native3 = require("lucide-react-native");
691
+ function AddNoteSheet({
692
+ snapPoints = ["50%"],
693
+ maxLength = 280,
694
+ placeholder = "Write a note...",
695
+ renderForm
696
+ }) {
697
+ const { isAddNoteOpen, addNoteTagId, activeLine, closeAddNote, submitNote } = useRetorBridge();
698
+ const sheetRef = (0, import_react6.useRef)(null);
699
+ const snapPointsArr = (0, import_react6.useMemo)(() => snapPoints, [snapPoints]);
700
+ const [text, setText] = (0, import_react6.useState)("");
701
+ const [isPrivate, setPrivate] = (0, import_react6.useState)(true);
702
+ (0, import_react6.useEffect)(() => {
703
+ if (isAddNoteOpen) {
704
+ sheetRef.current?.present();
705
+ setText("");
706
+ setPrivate(true);
707
+ } else {
708
+ sheetRef.current?.dismiss();
709
+ }
710
+ }, [isAddNoteOpen]);
711
+ const tag = (0, import_react6.useMemo)(
712
+ () => activeLine?.tags.find((t) => t._id === addNoteTagId) ?? null,
713
+ [activeLine, addNoteTagId]
714
+ );
715
+ const handleSubmit = () => {
716
+ if (!text.trim()) return;
717
+ submitNote(text.trim(), isPrivate);
718
+ setText("");
719
+ };
720
+ const formApi = {
721
+ text,
722
+ setText: (v) => {
723
+ if (v.length <= maxLength) setText(v);
724
+ },
725
+ isPrivate,
726
+ setPrivate,
727
+ submit: handleSubmit,
728
+ close: closeAddNote,
729
+ maxLength
730
+ };
731
+ return /* @__PURE__ */ import_react6.default.createElement(
732
+ import_bottom_sheet4.BottomSheetModal,
733
+ {
734
+ ref: sheetRef,
735
+ snapPoints: snapPointsArr,
736
+ enablePanDownToClose: true,
737
+ onDismiss: closeAddNote,
738
+ backdropComponent: (props) => /* @__PURE__ */ import_react6.default.createElement(
739
+ import_bottom_sheet4.BottomSheetBackdrop,
740
+ {
741
+ ...props,
742
+ appearsOnIndex: 0,
743
+ disappearsOnIndex: -1,
744
+ opacity: 0.6
745
+ }
746
+ ),
747
+ handleIndicatorStyle: styles4.handle,
748
+ backgroundStyle: styles4.background
749
+ },
750
+ /* @__PURE__ */ import_react6.default.createElement(import_bottom_sheet4.BottomSheetView, { style: styles4.content }, renderForm ? renderForm(formApi) : /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: styles4.form }, /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: styles4.headerRow }, /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: { flex: 1 } }, activeLine && /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles4.title, numberOfLines: 1 }, activeLine.name), tag && /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles4.subtitle, numberOfLines: 1 }, tag.name)), /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Pressable, { onPress: closeAddNote, style: styles4.closeBtn }, /* @__PURE__ */ import_react6.default.createElement(import_lucide_react_native3.X, { size: 14, color: "rgba(255,255,255,0.6)" }))), /* @__PURE__ */ import_react6.default.createElement(
751
+ import_bottom_sheet4.BottomSheetTextInput,
752
+ {
753
+ value: text,
754
+ onChangeText: (v) => {
755
+ if (v.length <= maxLength) setText(v);
756
+ },
757
+ placeholder,
758
+ placeholderTextColor: "rgba(255,255,255,0.3)",
759
+ multiline: true,
760
+ style: styles4.input,
761
+ autoFocus: true
206
762
  }
207
- );
208
- }
209
- );
763
+ ), /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: styles4.footer }, /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: [styles4.counter, text.length > maxLength * 0.9 && { color: "#fbbf24" }] }, text.length, "/", maxLength), /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: { flex: 1 } }), /* @__PURE__ */ import_react6.default.createElement(
764
+ import_react_native5.Pressable,
765
+ {
766
+ onPress: () => setPrivate(!isPrivate),
767
+ style: [styles4.pill, !isPrivate && styles4.pillActive]
768
+ },
769
+ /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: [styles4.pillText, !isPrivate && styles4.pillTextActive] }, isPrivate ? "Private" : "Public")
770
+ ), /* @__PURE__ */ import_react6.default.createElement(
771
+ import_react_native5.Pressable,
772
+ {
773
+ onPress: handleSubmit,
774
+ disabled: !text.trim(),
775
+ style: [styles4.submit, !text.trim() && styles4.submitDisabled]
776
+ },
777
+ /* @__PURE__ */ import_react6.default.createElement(import_lucide_react_native3.ArrowUp, { size: 16, color: "black", strokeWidth: 2.5 })
778
+ ))))
779
+ );
780
+ }
781
+ var styles4 = import_react_native5.StyleSheet.create({
782
+ background: { backgroundColor: "rgba(20,20,20,0.98)" },
783
+ handle: { backgroundColor: "rgba(255,255,255,0.3)" },
784
+ content: { flex: 1, padding: 24 },
785
+ form: { flex: 1 },
786
+ headerRow: { flexDirection: "row", alignItems: "flex-start", marginBottom: 16 },
787
+ title: { color: "white", fontSize: 18, fontWeight: "600" },
788
+ subtitle: { color: "rgba(255,255,255,0.5)", fontSize: 12, marginTop: 2 },
789
+ closeBtn: {
790
+ width: 28,
791
+ height: 28,
792
+ borderRadius: 14,
793
+ backgroundColor: "rgba(255,255,255,0.1)",
794
+ alignItems: "center",
795
+ justifyContent: "center"
796
+ },
797
+ input: {
798
+ flex: 1,
799
+ color: "white",
800
+ fontSize: 14,
801
+ minHeight: 100,
802
+ paddingVertical: 12
803
+ },
804
+ footer: {
805
+ flexDirection: "row",
806
+ alignItems: "center",
807
+ paddingTop: 12,
808
+ borderTopWidth: 1,
809
+ borderTopColor: "rgba(255,255,255,0.08)",
810
+ gap: 8
811
+ },
812
+ counter: { color: "rgba(255,255,255,0.4)", fontSize: 11 },
813
+ pill: {
814
+ paddingHorizontal: 12,
815
+ paddingVertical: 6,
816
+ borderRadius: 999,
817
+ backgroundColor: "rgba(255,255,255,0.1)"
818
+ },
819
+ pillActive: { backgroundColor: "rgba(59,130,246,0.2)" },
820
+ pillText: { color: "rgba(255,255,255,0.6)", fontSize: 11 },
821
+ pillTextActive: { color: "#60a5fa" },
822
+ submit: {
823
+ width: 32,
824
+ height: 32,
825
+ borderRadius: 16,
826
+ backgroundColor: "white",
827
+ alignItems: "center",
828
+ justifyContent: "center"
829
+ },
830
+ submitDisabled: { opacity: 0.3 }
831
+ });
832
+
833
+ // src/CoverPhoto.tsx
834
+ var import_react7 = __toESM(require("react"));
835
+ var import_react_native_webview2 = require("react-native-webview");
836
+ function CoverPhoto({ projectId, baseUrl = "https://retor.app", style }) {
837
+ const uri = `${baseUrl}/p/${projectId}?cover=true`;
838
+ return /* @__PURE__ */ import_react7.default.createElement(
839
+ import_react_native_webview2.WebView,
840
+ {
841
+ source: { uri },
842
+ style,
843
+ originWhitelist: ["*"],
844
+ javaScriptEnabled: true,
845
+ domStorageEnabled: true,
846
+ scrollEnabled: false
847
+ }
848
+ );
849
+ }
210
850
  // Annotate the CommonJS export names for ESM import in node:
211
851
  0 && (module.exports = {
852
+ AddNoteSheet,
853
+ CoverPhoto,
212
854
  Hud,
855
+ LineDetailSheet,
856
+ LineTagList,
857
+ LinesCarousel,
213
858
  Notes,
859
+ ProjectSheet,
214
860
  Viewer,
861
+ useActiveLine,
862
+ useAddNote,
863
+ useAutoplay,
864
+ useLineProgress,
865
+ useLines,
866
+ useProject,
867
+ useRetorBridge,
215
868
  useViewer
216
869
  });