@retor/react-native 0.1.0 → 0.2.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,462 @@ 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 } 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
+ function ProjectSheet({ snapPoints = ["20%", "60%"], renderHeader, children }) {
320
+ const { project, activeLineId, isAddNoteOpen } = useRetorBridge();
321
+ const sheetRef = useRef2(null);
322
+ const snapPointsArr = useMemo3(() => snapPoints, [snapPoints]);
323
+ useEffect2(() => {
324
+ if (activeLineId || isAddNoteOpen) {
325
+ sheetRef.current?.dismiss();
326
+ } else {
327
+ sheetRef.current?.present();
328
+ }
329
+ }, [activeLineId, isAddNoteOpen]);
330
+ return /* @__PURE__ */ React4.createElement(
331
+ BottomSheetModal,
332
+ {
333
+ ref: sheetRef,
334
+ snapPoints: snapPointsArr,
335
+ enablePanDownToClose: false,
336
+ enableDismissOnClose: false,
337
+ backdropComponent: (props) => /* @__PURE__ */ React4.createElement(
338
+ BottomSheetBackdrop,
339
+ {
340
+ ...props,
341
+ appearsOnIndex: 1,
342
+ disappearsOnIndex: 0,
343
+ opacity: 0.4,
344
+ pressBehavior: "collapse"
345
+ }
346
+ ),
347
+ handleIndicatorStyle: styles2.handle,
348
+ backgroundStyle: styles2.background
349
+ },
350
+ /* @__PURE__ */ React4.createElement(BottomSheetView, { style: styles2.content }, renderHeader ? renderHeader({ name: project?.name, description: project?.description }) : /* @__PURE__ */ React4.createElement(View3, { style: styles2.header }, project?.name && /* @__PURE__ */ React4.createElement(Text, { style: styles2.title }, project.name), project?.description && /* @__PURE__ */ React4.createElement(Text, { style: styles2.subtitle, numberOfLines: 4 }, project.description)), children ?? /* @__PURE__ */ React4.createElement(DefaultLinesCarousel, null))
351
+ );
352
+ }
353
+ function DefaultLinesCarousel() {
354
+ return /* @__PURE__ */ React4.createElement(LinesCarousel, null, (line) => /* @__PURE__ */ React4.createElement(DefaultLineCard, { line }));
355
+ }
356
+ function LinesCarousel({ children, gap = 12, paddingHorizontal = 16 }) {
357
+ const { lines } = useRetorBridge();
358
+ if (lines.length === 0) return null;
359
+ return /* @__PURE__ */ React4.createElement(
360
+ ScrollView,
361
+ {
362
+ horizontal: true,
363
+ showsHorizontalScrollIndicator: false,
364
+ contentContainerStyle: { paddingHorizontal, gap },
365
+ style: styles2.carousel
366
+ },
367
+ lines.map((line, idx) => /* @__PURE__ */ React4.createElement(React4.Fragment, { key: line._id }, children(line, idx)))
368
+ );
369
+ }
370
+ function DefaultLineCard({ line }) {
371
+ const { controls } = useRetorBridge();
372
+ return /* @__PURE__ */ React4.createElement(
373
+ Pressable,
374
+ {
375
+ onPress: () => controls.openLine(line._id),
376
+ style: styles2.lineCard
377
+ },
378
+ /* @__PURE__ */ React4.createElement(Text, { style: styles2.lineCardTitle, numberOfLines: 1 }, line.name || "Line"),
379
+ line.subtitle && /* @__PURE__ */ React4.createElement(Text, { style: styles2.lineCardSubtitle, numberOfLines: 2 }, line.subtitle)
380
+ );
381
+ }
382
+ var styles2 = StyleSheet3.create({
383
+ background: { backgroundColor: "rgba(20,20,20,0.95)" },
384
+ handle: { backgroundColor: "rgba(255,255,255,0.3)" },
385
+ content: { flex: 1, paddingTop: 12, paddingBottom: 24 },
386
+ header: { paddingHorizontal: 24, paddingBottom: 16 },
387
+ title: { color: "white", fontSize: 18, fontWeight: "600", lineHeight: 22 },
388
+ subtitle: { color: "rgba(255,255,255,0.6)", fontSize: 13, marginTop: 4, lineHeight: 18 },
389
+ carousel: { marginTop: 8 },
390
+ lineCard: {
391
+ width: 220,
392
+ backgroundColor: "rgba(255,255,255,0.06)",
393
+ borderRadius: 16,
394
+ padding: 16
395
+ },
396
+ lineCardTitle: { color: "white", fontSize: 14, fontWeight: "600" },
397
+ lineCardSubtitle: { color: "rgba(255,255,255,0.5)", fontSize: 11, marginTop: 4 }
398
+ });
399
+
400
+ // src/LineDetailSheet.tsx
401
+ import React5, { useEffect as useEffect3, useMemo as useMemo4, useRef as useRef3 } from "react";
402
+ import { Pressable as Pressable2, StyleSheet as StyleSheet4, Text as Text2, View as View4 } from "react-native";
403
+ import { BottomSheetBackdrop as BottomSheetBackdrop2, BottomSheetFlatList, BottomSheetModal as BottomSheetModal2, BottomSheetView as BottomSheetView2 } from "@gorhom/bottom-sheet";
404
+ function LineDetailSheet({ snapPoints = ["25%", "75%"], renderHeader, children }) {
405
+ const { activeLine, isAddNoteOpen, controls } = useRetorBridge();
406
+ const sheetRef = useRef3(null);
407
+ const snapPointsArr = useMemo4(() => snapPoints, [snapPoints]);
408
+ useEffect3(() => {
409
+ if (activeLine && !isAddNoteOpen) {
410
+ sheetRef.current?.present();
411
+ } else {
412
+ sheetRef.current?.dismiss();
413
+ }
414
+ }, [activeLine, isAddNoteOpen]);
415
+ return /* @__PURE__ */ React5.createElement(
416
+ BottomSheetModal2,
417
+ {
418
+ ref: sheetRef,
419
+ snapPoints: snapPointsArr,
420
+ enablePanDownToClose: false,
421
+ enableDismissOnClose: false,
422
+ backdropComponent: (props) => /* @__PURE__ */ React5.createElement(
423
+ BottomSheetBackdrop2,
424
+ {
425
+ ...props,
426
+ appearsOnIndex: 1,
427
+ disappearsOnIndex: 0,
428
+ opacity: 0.4,
429
+ pressBehavior: "collapse"
430
+ }
431
+ ),
432
+ handleIndicatorStyle: styles3.handle,
433
+ backgroundStyle: styles3.background
434
+ },
435
+ /* @__PURE__ */ React5.createElement(BottomSheetView2, { style: styles3.content }, activeLine && (renderHeader ? renderHeader(activeLine) : /* @__PURE__ */ React5.createElement(View4, { style: styles3.header }, /* @__PURE__ */ React5.createElement(Text2, { style: styles3.title }, activeLine.name), activeLine.subtitle && /* @__PURE__ */ React5.createElement(Text2, { style: styles3.subtitle }, activeLine.subtitle), activeLine.description && /* @__PURE__ */ React5.createElement(Text2, { style: styles3.description, numberOfLines: 4 }, activeLine.description))), 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"))))
436
+ );
437
+ }
438
+ function DefaultLineTagList() {
439
+ return /* @__PURE__ */ React5.createElement(LineTagList, null, (tag, isActive) => /* @__PURE__ */ React5.createElement(DefaultTagItem, { tag, isActive }));
440
+ }
441
+ function LineTagList({ children }) {
442
+ const { activeLine, closestTagId } = useRetorBridge();
443
+ const tags = useMemo4(
444
+ () => (activeLine?.tags ?? []).filter((t) => t.name && t.name.trim().length > 0),
445
+ [activeLine]
446
+ );
447
+ if (!activeLine) return null;
448
+ return /* @__PURE__ */ React5.createElement(
449
+ BottomSheetFlatList,
450
+ {
451
+ data: tags,
452
+ keyExtractor: (t) => t._id,
453
+ renderItem: ({ item }) => /* @__PURE__ */ React5.createElement(View4, null, children(item, item._id === closestTagId)),
454
+ contentContainerStyle: styles3.list
455
+ }
456
+ );
457
+ }
458
+ function DefaultTagItem({ tag, isActive }) {
459
+ const { controls } = useRetorBridge();
460
+ return /* @__PURE__ */ React5.createElement(
461
+ Pressable2,
462
+ {
463
+ onPress: () => controls.scrollToTag(tag._id),
464
+ style: [styles3.tagItem, isActive && styles3.tagItemActive]
465
+ },
466
+ /* @__PURE__ */ React5.createElement(Text2, { style: [styles3.tagText, isActive && styles3.tagTextActive], numberOfLines: 1 }, tag.name),
467
+ tag.subtitle && /* @__PURE__ */ React5.createElement(Text2, { style: styles3.tagSubtitle, numberOfLines: 1 }, tag.subtitle)
468
+ );
469
+ }
470
+ var styles3 = StyleSheet4.create({
471
+ background: { backgroundColor: "rgba(20,20,20,0.95)" },
472
+ handle: { backgroundColor: "rgba(255,255,255,0.3)" },
473
+ content: { flex: 1 },
474
+ header: { paddingHorizontal: 24, paddingTop: 8, paddingBottom: 12 },
475
+ title: { color: "white", fontSize: 20, fontWeight: "700" },
476
+ subtitle: { color: "rgba(255,255,255,0.5)", fontSize: 12, marginTop: 4 },
477
+ description: { color: "rgba(255,255,255,0.7)", fontSize: 13, marginTop: 8, lineHeight: 18 },
478
+ list: { paddingHorizontal: 16, paddingBottom: 8 },
479
+ tagItem: {
480
+ paddingHorizontal: 12,
481
+ paddingVertical: 10,
482
+ borderRadius: 12,
483
+ marginBottom: 4
484
+ },
485
+ tagItemActive: { backgroundColor: "rgba(255,255,255,0.1)" },
486
+ tagText: { color: "rgba(255,255,255,0.6)", fontSize: 14 },
487
+ tagTextActive: { color: "white", fontWeight: "600" },
488
+ tagSubtitle: { color: "rgba(255,255,255,0.4)", fontSize: 10, marginTop: 2 },
489
+ footer: { paddingHorizontal: 16, paddingBottom: 16, paddingTop: 8 },
490
+ doneButton: {
491
+ backgroundColor: "white",
492
+ borderRadius: 14,
493
+ paddingVertical: 14,
494
+ alignItems: "center"
495
+ },
496
+ doneText: { color: "black", fontSize: 14, fontWeight: "600" }
497
+ });
498
+
499
+ // src/AddNoteSheet.tsx
500
+ import React6, { useEffect as useEffect4, useMemo as useMemo5, useRef as useRef4, useState as useState2 } from "react";
501
+ import { Pressable as Pressable3, StyleSheet as StyleSheet5, Text as Text3, View as View5 } from "react-native";
502
+ import {
503
+ BottomSheetBackdrop as BottomSheetBackdrop3,
504
+ BottomSheetModal as BottomSheetModal3,
505
+ BottomSheetTextInput,
506
+ BottomSheetView as BottomSheetView3
507
+ } from "@gorhom/bottom-sheet";
508
+ function AddNoteSheet({
509
+ snapPoints = ["50%"],
510
+ maxLength = 280,
511
+ placeholder = "Write a note...",
512
+ renderForm
513
+ }) {
514
+ const { isAddNoteOpen, addNoteTagId, activeLine, closeAddNote, submitNote } = useRetorBridge();
515
+ const sheetRef = useRef4(null);
516
+ const snapPointsArr = useMemo5(() => snapPoints, [snapPoints]);
517
+ const [text, setText] = useState2("");
518
+ const [isPrivate, setPrivate] = useState2(true);
519
+ useEffect4(() => {
520
+ if (isAddNoteOpen) {
521
+ sheetRef.current?.present();
522
+ setText("");
523
+ setPrivate(true);
524
+ } else {
525
+ sheetRef.current?.dismiss();
526
+ }
527
+ }, [isAddNoteOpen]);
528
+ const tag = useMemo5(
529
+ () => activeLine?.tags.find((t) => t._id === addNoteTagId) ?? null,
530
+ [activeLine, addNoteTagId]
531
+ );
532
+ const handleSubmit = () => {
533
+ if (!text.trim()) return;
534
+ submitNote(text.trim(), isPrivate);
535
+ setText("");
536
+ };
537
+ const formApi = {
538
+ text,
539
+ setText: (v) => {
540
+ if (v.length <= maxLength) setText(v);
541
+ },
542
+ isPrivate,
543
+ setPrivate,
544
+ submit: handleSubmit,
545
+ close: closeAddNote,
546
+ maxLength
547
+ };
548
+ return /* @__PURE__ */ React6.createElement(
549
+ BottomSheetModal3,
550
+ {
551
+ ref: sheetRef,
552
+ snapPoints: snapPointsArr,
553
+ enablePanDownToClose: true,
554
+ onDismiss: closeAddNote,
555
+ backdropComponent: (props) => /* @__PURE__ */ React6.createElement(
556
+ BottomSheetBackdrop3,
557
+ {
558
+ ...props,
559
+ appearsOnIndex: 0,
560
+ disappearsOnIndex: -1,
561
+ opacity: 0.6
562
+ }
563
+ ),
564
+ handleIndicatorStyle: styles4.handle,
565
+ backgroundStyle: styles4.background
566
+ },
567
+ /* @__PURE__ */ React6.createElement(BottomSheetView3, { 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(Text3, { style: styles4.closeBtnText }, "\u2715"))), /* @__PURE__ */ React6.createElement(
568
+ BottomSheetTextInput,
167
569
  {
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
570
+ value: text,
571
+ onChangeText: (v) => {
572
+ if (v.length <= maxLength) setText(v);
573
+ },
574
+ placeholder,
575
+ placeholderTextColor: "rgba(255,255,255,0.3)",
576
+ multiline: true,
577
+ style: styles4.input,
578
+ autoFocus: true
177
579
  }
178
- );
179
- }
180
- );
580
+ ), /* @__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(
581
+ Pressable3,
582
+ {
583
+ onPress: () => setPrivate(!isPrivate),
584
+ style: [styles4.pill, !isPrivate && styles4.pillActive]
585
+ },
586
+ /* @__PURE__ */ React6.createElement(Text3, { style: [styles4.pillText, !isPrivate && styles4.pillTextActive] }, isPrivate ? "Private" : "Public")
587
+ ), /* @__PURE__ */ React6.createElement(
588
+ Pressable3,
589
+ {
590
+ onPress: handleSubmit,
591
+ disabled: !text.trim(),
592
+ style: [styles4.submit, !text.trim() && styles4.submitDisabled]
593
+ },
594
+ /* @__PURE__ */ React6.createElement(Text3, { style: styles4.submitArrow }, "\u2191")
595
+ ))))
596
+ );
597
+ }
598
+ var styles4 = StyleSheet5.create({
599
+ background: { backgroundColor: "rgba(20,20,20,0.98)" },
600
+ handle: { backgroundColor: "rgba(255,255,255,0.3)" },
601
+ content: { flex: 1, padding: 24 },
602
+ form: { flex: 1 },
603
+ headerRow: { flexDirection: "row", alignItems: "flex-start", marginBottom: 16 },
604
+ title: { color: "white", fontSize: 18, fontWeight: "600" },
605
+ subtitle: { color: "rgba(255,255,255,0.5)", fontSize: 12, marginTop: 2 },
606
+ closeBtn: {
607
+ width: 28,
608
+ height: 28,
609
+ borderRadius: 14,
610
+ backgroundColor: "rgba(255,255,255,0.1)",
611
+ alignItems: "center",
612
+ justifyContent: "center"
613
+ },
614
+ closeBtnText: { color: "rgba(255,255,255,0.6)", fontSize: 14 },
615
+ input: {
616
+ flex: 1,
617
+ color: "white",
618
+ fontSize: 14,
619
+ minHeight: 100,
620
+ paddingVertical: 12
621
+ },
622
+ footer: {
623
+ flexDirection: "row",
624
+ alignItems: "center",
625
+ paddingTop: 12,
626
+ borderTopWidth: 1,
627
+ borderTopColor: "rgba(255,255,255,0.08)",
628
+ gap: 8
629
+ },
630
+ counter: { color: "rgba(255,255,255,0.4)", fontSize: 11 },
631
+ pill: {
632
+ paddingHorizontal: 12,
633
+ paddingVertical: 6,
634
+ borderRadius: 999,
635
+ backgroundColor: "rgba(255,255,255,0.1)"
636
+ },
637
+ pillActive: { backgroundColor: "rgba(59,130,246,0.2)" },
638
+ pillText: { color: "rgba(255,255,255,0.6)", fontSize: 11 },
639
+ pillTextActive: { color: "#60a5fa" },
640
+ submit: {
641
+ width: 32,
642
+ height: 32,
643
+ borderRadius: 16,
644
+ backgroundColor: "white",
645
+ alignItems: "center",
646
+ justifyContent: "center"
647
+ },
648
+ submitDisabled: { opacity: 0.3 },
649
+ submitArrow: { color: "black", fontSize: 16, fontWeight: "700" }
650
+ });
651
+
652
+ // src/CoverPhoto.tsx
653
+ import React7 from "react";
654
+ import { WebView as WebView2 } from "react-native-webview";
655
+ function CoverPhoto({ projectId, baseUrl = "https://retor.app", style }) {
656
+ const uri = `${baseUrl}/p/${projectId}?cover=true`;
657
+ return /* @__PURE__ */ React7.createElement(
658
+ WebView2,
659
+ {
660
+ source: { uri },
661
+ style,
662
+ originWhitelist: ["*"],
663
+ javaScriptEnabled: true,
664
+ domStorageEnabled: true,
665
+ scrollEnabled: false
666
+ }
667
+ );
668
+ }
181
669
  export {
670
+ AddNoteSheet,
671
+ CoverPhoto,
182
672
  Hud,
673
+ LineDetailSheet,
674
+ LineTagList,
675
+ LinesCarousel,
183
676
  Notes,
677
+ ProjectSheet,
184
678
  Viewer,
679
+ useActiveLine,
680
+ useAddNote,
681
+ useAutoplay,
682
+ useLineProgress,
683
+ useLines,
684
+ useProject,
685
+ useRetorBridge,
185
686
  useViewer
186
687
  };