@retor/react-native 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,14 +6,16 @@ Embed Retor 3D experiences in a React Native / Expo app — with composable bott
6
6
 
7
7
  ```bash
8
8
  # Expo
9
- npx expo install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg
9
+ npx expo install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg expo-blur
10
10
  npm install @gorhom/bottom-sheet lucide-react-native @retor/react-native
11
11
 
12
12
  # bare React Native
13
- npm install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg @gorhom/bottom-sheet lucide-react-native @retor/react-native
13
+ npm install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg @gorhom/bottom-sheet lucide-react-native expo-blur @retor/react-native
14
14
  cd ios && pod install
15
15
  ```
16
16
 
17
+ > `expo-blur` is optional — it's used for the default blurred sheet background. Skip it if you don't want blur and pass a custom `backgroundComponent` to any sheet.
18
+
17
19
  You also need to wrap your app root in a `GestureHandlerRootView` (per `@gorhom/bottom-sheet` requirements):
18
20
 
19
21
  ```tsx
package/dist/index.d.mts CHANGED
@@ -220,6 +220,12 @@ interface LineTagListProps {
220
220
  /** Optional header rendered above the list (used internally for the default header). */
221
221
  listHeader?: React.ReactNode;
222
222
  }
223
+ /**
224
+ * Renders the tags of the active line as a vertical list inside a
225
+ * `BottomSheetScrollView`. Tracks each item's Y position via `onLayout`
226
+ * and scrolls the closest tag to the top of the visible area as the
227
+ * camera scrolls.
228
+ */
223
229
  declare function LineTagList({ children, listHeader }: LineTagListProps): React.JSX.Element | null;
224
230
 
225
231
  interface AddNoteSheetProps {
package/dist/index.d.ts CHANGED
@@ -220,6 +220,12 @@ interface LineTagListProps {
220
220
  /** Optional header rendered above the list (used internally for the default header). */
221
221
  listHeader?: React.ReactNode;
222
222
  }
223
+ /**
224
+ * Renders the tags of the active line as a vertical list inside a
225
+ * `BottomSheetScrollView`. Tracks each item's Y position via `onLayout`
226
+ * and scrolls the closest tag to the top of the visible area as the
227
+ * camera scrolls.
228
+ */
223
229
  declare function LineTagList({ children, listHeader }: LineTagListProps): React.JSX.Element | null;
224
230
 
225
231
  interface AddNoteSheetProps {
package/dist/index.js CHANGED
@@ -217,14 +217,10 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
217
217
  openLine: (lineId) => send("open-line", { lineId }),
218
218
  exitLine: () => send("exit-line"),
219
219
  scrollToTag: (tagId) => send("scroll-to-tag", { tagId }),
220
- toggleAutoplay: () => {
221
- setIsPlaying((v) => !v);
222
- send("toggle-autoplay");
223
- },
224
- setAutoplay: (playing) => {
225
- setIsPlaying(playing);
226
- send("set-autoplay", { playing });
227
- }
220
+ // Bridge owns autoplay state — it'll emit `autoplay-state` back, which
221
+ // we use to update isPlaying. No optimistic local update.
222
+ toggleAutoplay: () => send("toggle-autoplay"),
223
+ setAutoplay: (playing) => send("set-autoplay", { playing })
228
224
  }),
229
225
  [send]
230
226
  );
@@ -299,6 +295,11 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
299
295
  onLineProgress?.(payload);
300
296
  break;
301
297
  }
298
+ case "autoplay-state": {
299
+ const payload = data.payload;
300
+ setIsPlaying(!!payload.playing);
301
+ break;
302
+ }
302
303
  default:
303
304
  onMessage?.(data.type, data.payload);
304
305
  }
@@ -379,11 +380,15 @@ function ProjectSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
379
380
  (0, import_react5.useEffect)(() => {
380
381
  if (activeLineId || isAddNoteOpen) {
381
382
  sheetRef.current?.dismiss();
382
- } else {
383
+ return;
384
+ }
385
+ const t = setTimeout(() => {
383
386
  sheetRef.current?.present();
387
+ sheetRef.current?.snapToIndex(snapPointsArr.length - 1);
384
388
  setMinimized(false);
385
- }
386
- }, [activeLineId, isAddNoteOpen]);
389
+ }, 80);
390
+ return () => clearTimeout(t);
391
+ }, [activeLineId, isAddNoteOpen, snapPointsArr.length]);
387
392
  const handleSheetChange = (index) => {
388
393
  setMinimized(index === 0);
389
394
  };
@@ -498,13 +503,17 @@ function LineDetailSheet({ snapPoints = ["35%", "85%"], renderHeader, children }
498
503
  const snapPointsArr = (0, import_react6.useMemo)(() => snapPoints, [snapPoints]);
499
504
  const [minimized, setMinimized] = (0, import_react6.useState)(false);
500
505
  (0, import_react6.useEffect)(() => {
501
- if (activeLine && !isAddNoteOpen) {
502
- sheetRef.current?.present();
503
- setMinimized(false);
504
- } else {
506
+ if (!activeLine || isAddNoteOpen) {
505
507
  sheetRef.current?.dismiss();
508
+ return;
506
509
  }
507
- }, [activeLine, isAddNoteOpen]);
510
+ const t = setTimeout(() => {
511
+ sheetRef.current?.present();
512
+ sheetRef.current?.snapToIndex(snapPointsArr.length - 1);
513
+ setMinimized(false);
514
+ }, 80);
515
+ return () => clearTimeout(t);
516
+ }, [activeLine, isAddNoteOpen, snapPointsArr.length]);
508
517
  const handleSheetChange = (index) => {
509
518
  setMinimized(index === 0);
510
519
  };
@@ -574,45 +583,37 @@ function AutoplayButton() {
574
583
  }
575
584
  function LineTagList({ children, listHeader }) {
576
585
  const { activeLine, closestTagId } = useRetorBridge();
577
- const listRef = (0, import_react6.useRef)(null);
586
+ const scrollRef = (0, import_react6.useRef)(null);
587
+ const offsetsRef = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
578
588
  const tags = (0, import_react6.useMemo)(
579
589
  () => (activeLine?.tags ?? []).filter((t) => t.name && t.name.trim().length > 0),
580
590
  [activeLine]
581
591
  );
592
+ (0, import_react6.useEffect)(() => {
593
+ offsetsRef.current = /* @__PURE__ */ new Map();
594
+ }, [activeLine?._id]);
582
595
  (0, import_react6.useEffect)(() => {
583
596
  if (!closestTagId) return;
584
- const index = tags.findIndex((t2) => t2._id === closestTagId);
585
- if (index < 0) return;
586
597
  const t = setTimeout(() => {
587
- try {
588
- listRef.current?.scrollToIndex({ index, animated: true, viewPosition: 0 });
589
- } catch {
598
+ const y = offsetsRef.current.get(closestTagId);
599
+ if (y != null) {
600
+ scrollRef.current?.scrollTo({ y, animated: true });
590
601
  }
591
- }, 50);
602
+ }, 60);
592
603
  return () => clearTimeout(t);
593
- }, [closestTagId, tags]);
604
+ }, [closestTagId]);
594
605
  if (!activeLine) return null;
606
+ const handleItemLayout = (id, e) => {
607
+ offsetsRef.current.set(id, e.nativeEvent.layout.y);
608
+ };
595
609
  return /* @__PURE__ */ import_react6.default.createElement(
596
- import_bottom_sheet3.BottomSheetFlatList,
610
+ import_bottom_sheet3.BottomSheetScrollView,
597
611
  {
598
- ref: listRef,
599
- data: tags,
600
- keyExtractor: (t) => t._id,
601
- ListHeaderComponent: listHeader ? () => /* @__PURE__ */ import_react6.default.createElement(import_react6.default.Fragment, null, listHeader) : void 0,
602
- renderItem: ({ item }) => /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, null, children(item, item._id === closestTagId)),
603
- contentContainerStyle: styles3.list,
604
- removeClippedSubviews: false,
605
- onScrollToIndexFailed: (info) => {
606
- const offset = info.averageItemLength * info.index;
607
- listRef.current?.scrollToOffset({ offset, animated: false });
608
- setTimeout(() => {
609
- try {
610
- listRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 });
611
- } catch {
612
- }
613
- }, 100);
614
- }
615
- }
612
+ ref: scrollRef,
613
+ contentContainerStyle: styles3.list
614
+ },
615
+ listHeader,
616
+ tags.map((tag) => /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { key: tag._id, onLayout: (e) => handleItemLayout(tag._id, e) }, children(tag, tag._id === closestTagId)))
616
617
  );
617
618
  }
618
619
  function DefaultLineTagList({ listHeader }) {
package/dist/index.mjs CHANGED
@@ -173,14 +173,10 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
173
173
  openLine: (lineId) => send("open-line", { lineId }),
174
174
  exitLine: () => send("exit-line"),
175
175
  scrollToTag: (tagId) => send("scroll-to-tag", { tagId }),
176
- toggleAutoplay: () => {
177
- setIsPlaying((v) => !v);
178
- send("toggle-autoplay");
179
- },
180
- setAutoplay: (playing) => {
181
- setIsPlaying(playing);
182
- send("set-autoplay", { playing });
183
- }
176
+ // Bridge owns autoplay state — it'll emit `autoplay-state` back, which
177
+ // we use to update isPlaying. No optimistic local update.
178
+ toggleAutoplay: () => send("toggle-autoplay"),
179
+ setAutoplay: (playing) => send("set-autoplay", { playing })
184
180
  }),
185
181
  [send]
186
182
  );
@@ -255,6 +251,11 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
255
251
  onLineProgress?.(payload);
256
252
  break;
257
253
  }
254
+ case "autoplay-state": {
255
+ const payload = data.payload;
256
+ setIsPlaying(!!payload.playing);
257
+ break;
258
+ }
258
259
  default:
259
260
  onMessage?.(data.type, data.payload);
260
261
  }
@@ -335,11 +336,15 @@ function ProjectSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
335
336
  useEffect2(() => {
336
337
  if (activeLineId || isAddNoteOpen) {
337
338
  sheetRef.current?.dismiss();
338
- } else {
339
+ return;
340
+ }
341
+ const t = setTimeout(() => {
339
342
  sheetRef.current?.present();
343
+ sheetRef.current?.snapToIndex(snapPointsArr.length - 1);
340
344
  setMinimized(false);
341
- }
342
- }, [activeLineId, isAddNoteOpen]);
345
+ }, 80);
346
+ return () => clearTimeout(t);
347
+ }, [activeLineId, isAddNoteOpen, snapPointsArr.length]);
343
348
  const handleSheetChange = (index) => {
344
349
  setMinimized(index === 0);
345
350
  };
@@ -447,9 +452,9 @@ import React6, { useCallback as useCallback2, useEffect as useEffect3, useMemo a
447
452
  import { Pressable as Pressable2, StyleSheet as StyleSheet5, Text as Text2, View as View5 } from "react-native";
448
453
  import {
449
454
  BottomSheetBackdrop as BottomSheetBackdrop2,
450
- BottomSheetFlatList,
451
455
  BottomSheetFooter,
452
- BottomSheetModal as BottomSheetModal2
456
+ BottomSheetModal as BottomSheetModal2,
457
+ BottomSheetScrollView
453
458
  } from "@gorhom/bottom-sheet";
454
459
  import Svg, { Circle } from "react-native-svg";
455
460
  import { ArrowDown as ArrowDown2, ArrowUp as ArrowUp2, Pause, Play, Plus } from "lucide-react-native";
@@ -459,13 +464,17 @@ function LineDetailSheet({ snapPoints = ["35%", "85%"], renderHeader, children }
459
464
  const snapPointsArr = useMemo4(() => snapPoints, [snapPoints]);
460
465
  const [minimized, setMinimized] = useState3(false);
461
466
  useEffect3(() => {
462
- if (activeLine && !isAddNoteOpen) {
463
- sheetRef.current?.present();
464
- setMinimized(false);
465
- } else {
467
+ if (!activeLine || isAddNoteOpen) {
466
468
  sheetRef.current?.dismiss();
469
+ return;
467
470
  }
468
- }, [activeLine, isAddNoteOpen]);
471
+ const t = setTimeout(() => {
472
+ sheetRef.current?.present();
473
+ sheetRef.current?.snapToIndex(snapPointsArr.length - 1);
474
+ setMinimized(false);
475
+ }, 80);
476
+ return () => clearTimeout(t);
477
+ }, [activeLine, isAddNoteOpen, snapPointsArr.length]);
469
478
  const handleSheetChange = (index) => {
470
479
  setMinimized(index === 0);
471
480
  };
@@ -535,45 +544,37 @@ function AutoplayButton() {
535
544
  }
536
545
  function LineTagList({ children, listHeader }) {
537
546
  const { activeLine, closestTagId } = useRetorBridge();
538
- const listRef = useRef3(null);
547
+ const scrollRef = useRef3(null);
548
+ const offsetsRef = useRef3(/* @__PURE__ */ new Map());
539
549
  const tags = useMemo4(
540
550
  () => (activeLine?.tags ?? []).filter((t) => t.name && t.name.trim().length > 0),
541
551
  [activeLine]
542
552
  );
553
+ useEffect3(() => {
554
+ offsetsRef.current = /* @__PURE__ */ new Map();
555
+ }, [activeLine?._id]);
543
556
  useEffect3(() => {
544
557
  if (!closestTagId) return;
545
- const index = tags.findIndex((t2) => t2._id === closestTagId);
546
- if (index < 0) return;
547
558
  const t = setTimeout(() => {
548
- try {
549
- listRef.current?.scrollToIndex({ index, animated: true, viewPosition: 0 });
550
- } catch {
559
+ const y = offsetsRef.current.get(closestTagId);
560
+ if (y != null) {
561
+ scrollRef.current?.scrollTo({ y, animated: true });
551
562
  }
552
- }, 50);
563
+ }, 60);
553
564
  return () => clearTimeout(t);
554
- }, [closestTagId, tags]);
565
+ }, [closestTagId]);
555
566
  if (!activeLine) return null;
567
+ const handleItemLayout = (id, e) => {
568
+ offsetsRef.current.set(id, e.nativeEvent.layout.y);
569
+ };
556
570
  return /* @__PURE__ */ React6.createElement(
557
- BottomSheetFlatList,
571
+ BottomSheetScrollView,
558
572
  {
559
- ref: listRef,
560
- data: tags,
561
- keyExtractor: (t) => t._id,
562
- ListHeaderComponent: listHeader ? () => /* @__PURE__ */ React6.createElement(React6.Fragment, null, listHeader) : void 0,
563
- renderItem: ({ item }) => /* @__PURE__ */ React6.createElement(View5, null, children(item, item._id === closestTagId)),
564
- contentContainerStyle: styles3.list,
565
- removeClippedSubviews: false,
566
- onScrollToIndexFailed: (info) => {
567
- const offset = info.averageItemLength * info.index;
568
- listRef.current?.scrollToOffset({ offset, animated: false });
569
- setTimeout(() => {
570
- try {
571
- listRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 });
572
- } catch {
573
- }
574
- }, 100);
575
- }
576
- }
573
+ ref: scrollRef,
574
+ contentContainerStyle: styles3.list
575
+ },
576
+ listHeader,
577
+ tags.map((tag) => /* @__PURE__ */ React6.createElement(View5, { key: tag._id, onLayout: (e) => handleItemLayout(tag._id, e) }, children(tag, tag._id === closestTagId)))
577
578
  );
578
579
  }
579
580
  function DefaultLineTagList({ listHeader }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retor/react-native",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "React Native SDK for embedding Retor 3D experiences",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",