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