@neurodyn/core-ar 0.2.10 → 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
@@ -1,4 +1,3 @@
1
- import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock";
2
1
  let config_runtimeConfig;
3
2
  function normalizeHttpOrigin(value, name) {
4
3
  let url;
@@ -10,11 +9,6 @@ function normalizeHttpOrigin(value, name) {
10
9
  if ('http:' !== url.protocol && 'https:' !== url.protocol) throw new TypeError(`${name} must be an absolute HTTP(S) URL`);
11
10
  return url.origin;
12
11
  }
13
- function normalizeHttpBaseUrl(value, name) {
14
- const origin = normalizeHttpOrigin(value, name);
15
- const pathname = new URL(value).pathname.replace(/\/+$/, '');
16
- return pathname ? `${origin}${pathname}` : origin;
17
- }
18
12
  function deriveModelViewerOrigin(viewerOrigin) {
19
13
  const url = new URL(viewerOrigin);
20
14
  const match = url.hostname.match(/^viewer(-dev)?\.(vizbl\.com|arizo\.ru)$/);
@@ -25,13 +19,11 @@ function resolveCoreArWidgetRuntimeConfig(config) {
25
19
  const apiOrigin = normalizeHttpOrigin(config.apiOrigin, 'apiOrigin');
26
20
  const viewerOrigin = normalizeHttpOrigin(config.viewerOrigin, 'viewerOrigin');
27
21
  const modelViewerOrigin = config.modelViewerOrigin ? normalizeHttpOrigin(config.modelViewerOrigin, 'modelViewerOrigin') : deriveModelViewerOrigin(viewerOrigin);
28
- const legacyApiOrigin = config.legacyApiOrigin ? normalizeHttpBaseUrl(config.legacyApiOrigin, 'legacyApiOrigin') : `${apiOrigin}/v1`;
29
22
  const brand = new URL(viewerOrigin).hostname.endsWith('arizo.ru') ? 'arizo' : 'vizbl';
30
23
  return {
31
24
  apiOrigin,
32
25
  brand,
33
26
  fetchImpl: config.fetchImpl ?? fetch,
34
- legacyApiOrigin,
35
27
  modelViewerOrigin,
36
28
  qrCodeScriptUrl: config.qrCodeScriptUrl ?? `${viewerOrigin}/vendor/qrcode.min.js`,
37
29
  viewerOrigin
@@ -61,6 +53,7 @@ function isIOS() {
61
53
  }
62
54
  function isMobile() {
63
55
  if ("u" < typeof window || "u" < typeof navigator) return false;
56
+ if (isIOS()) return true;
64
57
  const hasMultiTouch = navigator.maxTouchPoints > 1;
65
58
  const isCoarsePointer = window.matchMedia?.('(pointer: coarse)').matches ?? false;
66
59
  const isMobileByUserAgent = /Android|iPhone|iPad|iPod|webOS|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile/i.test(getUserAgent());
@@ -612,10 +605,10 @@ async function sendWidgetEvent(event, sessionToken, objectUuid, options = {}) {
612
605
  const data = await parseSuccessJson(response);
613
606
  if (!data || 'object' != typeof data || Array.isArray(data) || 'boolean' != typeof data.ok) throw new WidgetRuntimeError('INVALID_RESPONSE');
614
607
  }
615
- async function fetchLegacyObject(objectId, signal, runtimeConfig) {
608
+ async function fetchPublicObject(objectId, signal, runtimeConfig) {
616
609
  const resolvedConfig = runtimeConfig ?? getCoreArWidgetRuntimeConfig();
617
- const { fetchImpl, legacyApiOrigin } = resolvedConfig;
618
- const response = await runtimeFetch(`${legacyApiOrigin}/obj/Fetch`, {
610
+ const { apiOrigin, fetchImpl } = resolvedConfig;
611
+ const response = await runtimeFetch(`${apiOrigin}/catalog/object`, {
619
612
  body: JSON.stringify({
620
613
  tinuuid: objectId
621
614
  }),
@@ -633,9 +626,9 @@ async function fetchLegacyObject(objectId, signal, runtimeConfig) {
633
626
  if (!data || 'object' != typeof data || Array.isArray(data)) throw new WidgetRuntimeError('INVALID_RESPONSE');
634
627
  return data;
635
628
  }
636
- async function incrementLegacyObjectView(objectId, runtimeConfig) {
637
- const { fetchImpl, legacyApiOrigin } = runtimeConfig ?? getCoreArWidgetRuntimeConfig();
638
- await fetchImpl(`${legacyApiOrigin}/obj/IncrViews`, {
629
+ async function incrementObjectViews(objectId, runtimeConfig) {
630
+ const { apiOrigin, fetchImpl } = runtimeConfig ?? getCoreArWidgetRuntimeConfig();
631
+ await fetchImpl(`${apiOrigin}/catalog/object/incr-views`, {
639
632
  body: JSON.stringify({
640
633
  tinuuid: objectId
641
634
  }),
@@ -662,7 +655,7 @@ async function initializeAdvancedArTrigger(trigger, options = {}) {
662
655
  const variantId = trigger.dataset.objectHid?.trim() || void 0;
663
656
  if (!objectId) throw new Error('Object ID is required');
664
657
  abortIfNeeded(options.signal);
665
- const loadObject = options.loadObject ?? ((id, signal)=>fetchLegacyObject(id, signal, options.runtimeConfig));
658
+ const loadObject = options.loadObject ?? ((id, signal)=>fetchPublicObject(id, signal, options.runtimeConfig));
666
659
  const object = options.object ?? await loadObject(objectId, options.signal);
667
660
  abortIfNeeded(options.signal);
668
661
  if (!initialization.isCurrent()) return ()=>void 0;
@@ -680,7 +673,7 @@ async function initializeAdvancedArTrigger(trigger, options = {}) {
680
673
  createdImage.hidden = true;
681
674
  trigger.appendChild(createdImage);
682
675
  }
683
- const trackView = ()=>options.trackView ? options.trackView() : incrementLegacyObjectView(objectId, options.runtimeConfig);
676
+ const trackView = ()=>options.trackView ? options.trackView() : incrementObjectViews(objectId, options.runtimeConfig);
684
677
  const handleDesktopClick = (event)=>{
685
678
  event.preventDefault();
686
679
  const revision = ++qrRevision;
@@ -732,6 +725,52 @@ async function initializeAdvancedArTrigger(trigger, options = {}) {
732
725
  else trigger.setAttribute('data-native-link', originalNativeLink);
733
726
  });
734
727
  }
728
+ const DIALOG_EXIT_MS = 300;
729
+ let pendingLoad;
730
+ async function importModelViewerDialog() {
731
+ const [{ ModelViewerDialog }, { createElement }, { createRoot }] = await Promise.all([
732
+ import("@neurodyn/react-model-viewer"),
733
+ import("react"),
734
+ import("react-dom/client")
735
+ ]);
736
+ return {
737
+ open: ({ onClose, ...props })=>{
738
+ const container = document.createElement('div');
739
+ container.setAttribute('data-core-ar-model-viewer', '');
740
+ document.body.appendChild(container);
741
+ const root = createRoot(container);
742
+ let closed = false;
743
+ const render = (open)=>root.render(createElement(ModelViewerDialog, {
744
+ ...props,
745
+ onOpenChange: (next)=>{
746
+ if (!next) close();
747
+ },
748
+ open
749
+ }));
750
+ function close() {
751
+ if (closed) return;
752
+ closed = true;
753
+ render(false);
754
+ setTimeout(()=>{
755
+ root.unmount();
756
+ container.remove();
757
+ }, DIALOG_EXIT_MS);
758
+ onClose?.();
759
+ }
760
+ render(true);
761
+ return close;
762
+ }
763
+ };
764
+ }
765
+ function loadModelViewerDialog() {
766
+ if (pendingLoad) return pendingLoad;
767
+ const promise = importModelViewerDialog();
768
+ pendingLoad = promise;
769
+ promise.catch(()=>{
770
+ if (pendingLoad === promise) pendingLoad = void 0;
771
+ });
772
+ return promise;
773
+ }
735
774
  const SIZE_TYPES = [
736
775
  'cm',
737
776
  'inch'
@@ -745,7 +784,6 @@ const DEFAULT_SIZE_TYPE = 'inch';
745
784
  const ALL_DIMENSION_AXES = [
746
785
  ...DIMENSION_AXES
747
786
  ];
748
- const VIEWER_CONTAINER_ID = 'vizbl_viewer';
749
787
  const modal_trigger_beginInitialization = createElementInitializerRegistry();
750
788
  function resolveSizeType(value) {
751
789
  return value && SIZE_TYPES.includes(value) ? value : DEFAULT_SIZE_TYPE;
@@ -774,177 +812,60 @@ function createModalViewerUrl(params) {
774
812
  url.hash = fragment.toString();
775
813
  return url.toString();
776
814
  }
777
- function createFrame(src, backgroundColor) {
778
- const frame = document.createElement('iframe');
779
- frame.src = src;
780
- frame.title = '3D model viewer';
781
- frame.allow = 'fullscreen; xr-spatial-tracking';
782
- Object.assign(frame.style, {
783
- backgroundColor: backgroundColor ?? 'white',
784
- border: 'none',
785
- height: '100%',
786
- inset: '0',
787
- position: 'absolute',
788
- width: '100%',
789
- zIndex: '11112'
790
- });
791
- return frame;
792
- }
793
- function createCloseButton(className, close, lang = 'en') {
794
- const button = document.createElement('button');
795
- button.type = 'button';
796
- button.setAttribute('aria-label', 'ru' === lang ? 'Закрыть просмотр 3D-модели' : 'Close 3D model viewer');
797
- if (className) button.className = className;
798
- Object.assign(button.style, {
799
- alignItems: 'center',
800
- background: '#fff',
801
- border: '1px solid #E3E7E7',
802
- borderRadius: '4px',
803
- boxSizing: 'border-box',
804
- cursor: 'pointer',
805
- display: 'flex',
806
- height: '40px',
807
- inset: '24px 24px auto auto',
808
- justifyContent: 'center',
809
- padding: '8px 16px',
810
- position: 'absolute',
811
- width: '56px',
812
- zIndex: '11113'
813
- });
814
- button.innerHTML = '<svg aria-hidden="true" style="flex-shrink:0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z" fill="#484D50"/></svg>';
815
- button.addEventListener('click', close);
816
- return button;
817
- }
818
- function openModal(options) {
819
- if (document.getElementById(VIEWER_CONTAINER_ID)) return null;
820
- const root = document.createElement('div');
821
- const viewer = document.createElement('div');
822
- const overlay = document.createElement('div');
823
- const targetContainer = options.containerClassName ? document.getElementsByClassName(options.containerClassName)[0] : null;
824
- const isInlineContainer = Boolean(targetContainer);
825
- const lockTarget = isInlineContainer ? viewer : root;
826
- let closed = false;
827
- let close;
828
- const handleKeydown = (event)=>{
829
- if ('Escape' === event.key) close();
830
- };
831
- close = ()=>{
832
- if (closed) return;
833
- closed = true;
834
- document.removeEventListener('keydown', handleKeydown);
835
- enableBodyScroll(lockTarget);
836
- (isInlineContainer ? viewer : root).remove();
837
- options.onClose?.();
838
- options.restoreFocusTo?.focus({
839
- preventScroll: true
840
- });
841
- };
842
- root.id = VIEWER_CONTAINER_ID;
843
- Object.assign(root.style, {
844
- inset: '0',
845
- overflow: 'auto',
846
- position: 'fixed',
847
- zIndex: '2147483647'
848
- });
849
- root.style.webkitOverflowScrolling = 'touch';
850
- viewer.setAttribute('role', 'dialog');
851
- viewer.setAttribute('aria-modal', 'true');
852
- viewer.setAttribute('aria-label', 'ru' === options.lang ? 'Просмотр 3D-модели' : '3D model viewer');
853
- Object.assign(overlay.style, {
854
- animation: 'vex-fadein 0.5s',
855
- background: '#fff',
856
- inset: '0',
857
- position: 'fixed',
858
- zIndex: '11111'
859
- });
860
- overlay.addEventListener('click', close);
861
- if (options.width || options.height) {
862
- overlay.style.background = 'rgba(0, 0, 0, 0.8)';
863
- Object.assign(viewer.style, {
864
- borderRadius: '12px',
865
- height: options.height ?? '100%',
866
- left: '50%',
867
- overflow: 'hidden',
868
- position: 'absolute',
869
- top: '50%',
870
- transform: 'translate(-50%, -50%)',
871
- width: options.width ?? '100%',
872
- zIndex: '11112'
873
- });
874
- }
875
- const closeButton = createCloseButton(options.crossClassName, close, options.lang);
876
- viewer.append(createFrame(createModalViewerUrl(options), options.backgroundColor), closeButton);
877
- if (targetContainer) {
878
- viewer.id = VIEWER_CONTAINER_ID;
879
- targetContainer.appendChild(viewer);
880
- } else {
881
- root.append(overlay, viewer);
882
- document.body.appendChild(root);
883
- }
884
- document.addEventListener('keydown', handleKeydown);
885
- disableBodyScroll(lockTarget);
886
- closeButton.focus({
887
- preventScroll: true
888
- });
889
- return close;
890
- }
891
- function initializeModalTrigger(trigger, options = {}) {
815
+ function initializeModalTrigger(trigger, options) {
892
816
  const initialization = modal_trigger_beginInitialization(trigger);
893
- let closeOwnedModal;
817
+ let closeOwnedViewer;
894
818
  let pendingOpen = false;
895
819
  const handleClick = async ()=>{
896
- if (closeOwnedModal || pendingOpen) return;
820
+ if (closeOwnedViewer || pendingOpen) return;
897
821
  const objectId = trigger.dataset.objectTinuuid?.trim();
898
822
  if (!objectId) return;
899
823
  pendingOpen = true;
900
- let widgetSessionToken = options.widgetSessionToken;
824
+ runSafely(()=>options.onLoading?.());
901
825
  try {
902
- if (options.getWidgetSessionToken) widgetSessionToken = await options.getWidgetSessionToken();
903
- } catch {
904
- pendingOpen = false;
826
+ const widgetSessionToken = options.getWidgetSessionToken ? await options.getWidgetSessionToken() : options.widgetSessionToken;
827
+ const viewer = await loadModelViewerDialog();
828
+ if (closeOwnedViewer || !initialization.isCurrent()) return;
829
+ const modelViewerOrigin = options.modelViewerOrigin ?? getCoreArWidgetRuntimeConfig().modelViewerOrigin;
830
+ let closed = false;
831
+ closeOwnedViewer = viewer.open({
832
+ apiBaseUrl: options.viewer.apiBaseUrl,
833
+ apiKey: options.apiKey,
834
+ cameraOrbit: trigger.dataset.cameraOrbit,
835
+ dimensionAxes: resolveDimensionAxes(trigger.dataset.dimensionAxes),
836
+ handoffUrl: new URL('/model-viewer', modelViewerOrigin).toString(),
837
+ hid: trigger.dataset.objectHid,
838
+ initialWidgetSessionToken: widgetSessionToken,
839
+ lang: options.lang,
840
+ maxCameraOrbit: trigger.dataset.maxCameraOrbit,
841
+ minCameraOrbit: trigger.dataset.minCameraOrbit,
842
+ mode: options.mode,
843
+ onClose: ()=>{
844
+ if (closed) return;
845
+ closed = true;
846
+ closeOwnedViewer = void 0;
847
+ options.onClose?.();
848
+ trigger.focus({
849
+ preventScroll: true
850
+ });
851
+ },
852
+ sizeType: resolveSizeType(trigger.dataset.sizeType),
853
+ tinuuid: objectId
854
+ });
855
+ } catch (error) {
856
+ if (initialization.isCurrent()) runSafely(()=>options.onError?.(error));
905
857
  return;
858
+ } finally{
859
+ pendingOpen = false;
906
860
  }
907
- pendingOpen = false;
908
- if (closeOwnedModal || !initialization.isCurrent()) return;
909
- const opened = openModal({
910
- apiKey: options.apiKey,
911
- backgroundColor: trigger.dataset.background,
912
- buttonBgColor: trigger.dataset.buttonBgColor,
913
- buttonBorderColor: trigger.dataset.buttonBorderColor,
914
- buttonTextColor: trigger.dataset.buttonTextColor,
915
- cameraOrbit: trigger.dataset.cameraOrbit,
916
- containerClassName: trigger.dataset.container,
917
- crossClassName: trigger.dataset.crossClassName,
918
- dimensionAxes: resolveDimensionAxes(trigger.dataset.dimensionAxes),
919
- height: trigger.dataset.height?.trim(),
920
- hid: trigger.dataset.objectHid,
921
- hidePreview: trigger.dataset.hidePreview,
922
- hideTitle: trigger.dataset.hideTitle,
923
- lang: options.lang,
924
- maxCameraOrbit: trigger.dataset.maxCameraOrbit,
925
- minCameraOrbit: trigger.dataset.minCameraOrbit,
926
- mode: options.mode,
927
- modelViewerOrigin: options.modelViewerOrigin,
928
- onClose: ()=>{
929
- closeOwnedModal = void 0;
930
- options.onClose?.();
931
- },
932
- restoreFocusTo: trigger,
933
- sizeType: resolveSizeType(trigger.dataset.sizeType),
934
- tinuuid: objectId,
935
- widgetSessionToken,
936
- width: trigger.dataset.width?.trim()
937
- });
938
- if (!opened) return;
939
- closeOwnedModal = opened;
940
861
  options.onOpen?.();
941
862
  runSafely(()=>options.onAction?.('widget_opened'));
942
863
  };
943
864
  trigger.addEventListener('click', handleClick);
944
865
  return initialization.register(()=>{
945
866
  trigger.removeEventListener('click', handleClick);
946
- closeOwnedModal?.();
947
- closeOwnedModal = void 0;
867
+ closeOwnedViewer?.();
868
+ closeOwnedViewer = void 0;
948
869
  });
949
870
  }
950
871
  const native_ar_anchor_beginInitialization = createElementInitializerRegistry();
@@ -959,7 +880,7 @@ async function initializeNativeArAnchor(link, options = {}) {
959
880
  const variantId = link.dataset.objectHid?.trim() || void 0;
960
881
  if (!objectId) throw new Error('Object ID is required');
961
882
  native_ar_anchor_abortIfNeeded(options.signal);
962
- const loadObject = options.loadObject ?? ((id, signal)=>fetchLegacyObject(id, signal, options.runtimeConfig));
883
+ const loadObject = options.loadObject ?? ((id, signal)=>fetchPublicObject(id, signal, options.runtimeConfig));
963
884
  const object = options.object ?? await loadObject(objectId, options.signal);
964
885
  native_ar_anchor_abortIfNeeded(options.signal);
965
886
  if (!initialization.isCurrent()) return ()=>void 0;
@@ -993,7 +914,7 @@ async function initializeNativeArAnchor(link, options = {}) {
993
914
  }).catch(()=>void 0);
994
915
  };
995
916
  const handleMobileClick = ()=>{
996
- runSafely(options.trackView ?? (()=>incrementLegacyObjectView(objectId, options.runtimeConfig)));
917
+ runSafely(options.trackView ?? (()=>incrementObjectViews(objectId, options.runtimeConfig)));
997
918
  runSafely(()=>options.onAction?.('ar_launched'));
998
919
  };
999
920
  if (mobile) {
@@ -1148,6 +1069,31 @@ function installScopedStyles() {
1148
1069
  :where(vizbl-core, arizo-core)[data-state="error"] [data-core-ar-widget-trigger] {
1149
1070
  cursor: not-allowed;
1150
1071
  }
1072
+
1073
+ :where(vizbl-core, arizo-core):not([unstyled])[data-state="loading"] [data-core-ar-widget-trigger] {
1074
+ color: transparent;
1075
+ position: relative;
1076
+ }
1077
+
1078
+ :where(vizbl-core, arizo-core):not([unstyled])[data-state="loading"] [data-core-ar-widget-trigger]::after {
1079
+ animation: core-ar-widget-spin 700ms linear infinite;
1080
+ border: 2px solid var(--core-ar-widget-color);
1081
+ border-radius: 50%;
1082
+ border-right-color: transparent;
1083
+ box-sizing: border-box;
1084
+ content: "";
1085
+ height: 1em;
1086
+ inset: 0;
1087
+ margin: auto;
1088
+ position: absolute;
1089
+ width: 1em;
1090
+ }
1091
+
1092
+ @keyframes core-ar-widget-spin {
1093
+ to {
1094
+ transform: rotate(360deg);
1095
+ }
1096
+ }
1151
1097
  `;
1152
1098
  document.head.appendChild(style);
1153
1099
  }
@@ -1275,13 +1221,13 @@ class CoreArWidgetElement extends HTMLElementBase {
1275
1221
  this.applyIntegrationData(objectId, variantId, sizeType, dimensionAxes, cameraOrbit, minCameraOrbit, maxCameraOrbit);
1276
1222
  let object;
1277
1223
  if ('ar-viewer' === rawType) {
1278
- object = await fetchLegacyObject(objectId, abortController.signal, runtimeConfig);
1224
+ object = await fetchPublicObject(objectId, abortController.signal, runtimeConfig);
1279
1225
  this.assertCurrent(revision, abortController.signal);
1280
1226
  getSelectedMaterial(object, variantId);
1281
1227
  assertPolicy(rawType, object);
1282
1228
  this.object = object;
1283
1229
  } else {
1284
- object = isMobile() ? await this.fetchObjectWithSession(objectId, publicKey, session, runtimeConfig, abortController.signal, variantId) : await fetchLegacyObject(objectId, abortController.signal, runtimeConfig);
1230
+ object = isMobile() ? await this.fetchObjectWithSession(objectId, publicKey, session, runtimeConfig, abortController.signal, variantId) : await fetchPublicObject(objectId, abortController.signal, runtimeConfig);
1285
1231
  this.assertCurrent(revision, abortController.signal);
1286
1232
  const material = getSelectedMaterial(object, variantId);
1287
1233
  assertPolicy(rawType, object);
@@ -1313,6 +1259,16 @@ class CoreArWidgetElement extends HTMLElementBase {
1313
1259
  });
1314
1260
  }
1315
1261
  }
1262
+ failInteraction(code, type) {
1263
+ if (!this.isConnected) return;
1264
+ this.setAttribute('data-error-code', code);
1265
+ this.setState('error');
1266
+ this.dispatchWidgetEvent('core-ar-error', {
1267
+ code,
1268
+ objectId: this.getAttribute('object-id')?.trim() ?? '',
1269
+ type
1270
+ });
1271
+ }
1316
1272
  async ensureSession(publicKey, runtimeConfig, mode, signal, forceRefresh = false) {
1317
1273
  const runtimeKey = `${runtimeConfig.apiOrigin}|${runtimeConfig.viewerOrigin}|${runtimeConfig.modelViewerOrigin}|${mode}`;
1318
1274
  const existing = this.session?.publicKey === publicKey && this.session.runtimeKey === runtimeKey ? this.session : void 0;
@@ -1440,7 +1396,12 @@ class CoreArWidgetElement extends HTMLElementBase {
1440
1396
  onClose: ()=>{
1441
1397
  if (this.isConnected) this.setState('ready');
1442
1398
  },
1399
+ onError: ()=>this.failInteraction('VIEWER_UNAVAILABLE', type),
1400
+ onLoading: ()=>this.setState('loading'),
1443
1401
  onOpen: ()=>this.setState('opening'),
1402
+ viewer: {
1403
+ apiBaseUrl: runtimeConfig.apiOrigin
1404
+ },
1444
1405
  widgetSessionToken: session.token
1445
1406
  });
1446
1407
  if (!object) throw new CoreArWidgetComponentError('INITIALIZATION_FAILED');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@neurodyn/core-ar",
3
3
  "type": "module",
4
- "version": "0.2.10",
4
+ "version": "0.3.0",
5
5
  "sideEffects": false,
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -20,18 +20,22 @@
20
20
  "dist"
21
21
  ],
22
22
  "dependencies": {
23
- "body-scroll-lock": "^4.0.0-beta.0"
23
+ "react": "^19.2.0",
24
+ "react-dom": "^19.2.0",
25
+ "@neurodyn/react-model-viewer": "0.1.0"
24
26
  },
25
27
  "devDependencies": {
28
+ "@rsbuild/core": "1.7.1",
26
29
  "@rslib/core": "^0.18.6",
27
- "@types/body-scroll-lock": "^3.1.2",
30
+ "@types/react": "^19.2.4",
31
+ "@types/react-dom": "19.2.3",
28
32
  "eslint": "9.39.1",
29
33
  "happy-dom": "^17.6.3",
30
34
  "typescript": "^5.9.3",
31
35
  "vitest": "^3.0.5"
32
36
  },
33
37
  "scripts": {
34
- "build": "rslib build",
38
+ "build": "rslib build && rsbuild build",
35
39
  "dev": "rslib build --watch",
36
40
  "test": "vitest run",
37
41
  "lint": "eslint .",