@neurodyn/react-room-viewer 0.0.12 → 0.0.14

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.d.ts CHANGED
@@ -86,6 +86,11 @@ declare const resources: {
86
86
  readonly unauthorized: "Invalid API key";
87
87
  readonly 'payment-required': "Viewer temporarily unavailable";
88
88
  };
89
+ readonly loadError: {
90
+ readonly 'viewer-load-failed': "We could not load the viewer. Please try again.";
91
+ readonly 'artwork-load-failed': "The image could not be loaded — its link is missing or incorrect in the widget settings.";
92
+ readonly 'room-load-failed': "The room photo could not be loaded. Please try again.";
93
+ };
89
94
  readonly resourceType: {
90
95
  readonly rugs: {
91
96
  readonly productCategory: "Rug";
@@ -200,6 +205,11 @@ declare const resources: {
200
205
  readonly unauthorized: "Неверный API-ключ";
201
206
  readonly 'payment-required': "Просмотр временно недоступен";
202
207
  };
208
+ readonly loadError: {
209
+ readonly 'viewer-load-failed': "Не удалось загрузить просмотр. Попробуйте ещё раз.";
210
+ readonly 'artwork-load-failed': "Не удалось загрузить изображение — ссылка на него отсутствует или указана неверно в настройках виджета.";
211
+ readonly 'room-load-failed': "Не удалось загрузить фото комнаты. Попробуйте ещё раз.";
212
+ };
203
213
  readonly resourceType: {
204
214
  readonly rugs: {
205
215
  readonly productCategory: "Ковер";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx as jsx_runtime_jsx, jsxs } from "react/jsx-runtime";
2
- import { Suspense, createContext, forwardRef, use as external_react_use, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo as external_react_useMemo, useReducer, useRef, useState } from "react";
2
+ import { Component, Suspense, createContext, forwardRef, use as external_react_use, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo as external_react_useMemo, useReducer, useRef, useState } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { I18nextProvider, initReactI18next, useTranslation } from "react-i18next";
5
5
  import react_shadow from "react-shadow";
@@ -19,7 +19,8 @@ import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
19
19
  import { Content as react_tabs_Content, List, Root as react_tabs_Root, Trigger } from "@radix-ui/react-tabs";
20
20
  import { formatHex, oklch } from "culori";
21
21
  import qrcode from "qrcode";
22
- import { GLTFExporter, USDZExporter } from "three/examples/jsm/Addons.js";
22
+ import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
23
+ import { USDZExporter } from "three/examples/jsm/exporters/USDZExporter.js";
23
24
  import "@google/model-viewer";
24
25
  import { Clone, DragControls, Environment, useGLTF, useProgress, useTexture } from "@react-three/drei";
25
26
  import { Canvas, useLoader, useThree } from "@react-three/fiber";
@@ -1359,6 +1360,10 @@ var __webpack_modules__ = {
1359
1360
  height: auto;
1360
1361
  }
1361
1362
 
1363
+ .h-dvh {
1364
+ height: 100dvh;
1365
+ }
1366
+
1362
1367
  .h-full {
1363
1368
  height: 100%;
1364
1369
  }
@@ -1371,10 +1376,6 @@ var __webpack_modules__ = {
1371
1376
  height: 1px;
1372
1377
  }
1373
1378
 
1374
- .h-svh {
1375
- height: 100svh;
1376
- }
1377
-
1378
1379
  .max-h-\\(--radix-select-content-available-height\\) {
1379
1380
  max-height: var(--radix-select-content-available-height);
1380
1381
  }
@@ -6640,6 +6641,11 @@ const ns1 = {
6640
6641
  unauthorized: 'Invalid API key',
6641
6642
  'payment-required': 'Viewer temporarily unavailable'
6642
6643
  },
6644
+ loadError: {
6645
+ 'viewer-load-failed': 'We could not load the viewer. Please try again.',
6646
+ 'artwork-load-failed': 'The image could not be loaded — its link is missing or incorrect in the widget settings.',
6647
+ 'room-load-failed': 'The room photo could not be loaded. Please try again.'
6648
+ },
6643
6649
  resourceType: {
6644
6650
  rugs: {
6645
6651
  productCategory: 'Rug',
@@ -6753,6 +6759,11 @@ const ns1_ns1 = {
6753
6759
  unauthorized: 'Неверный API-ключ',
6754
6760
  'payment-required': 'Просмотр временно недоступен'
6755
6761
  },
6762
+ loadError: {
6763
+ 'viewer-load-failed': 'Не удалось загрузить просмотр. Попробуйте ещё раз.',
6764
+ 'artwork-load-failed': 'Не удалось загрузить изображение — ссылка на него отсутствует или указана неверно в настройках виджета.',
6765
+ 'room-load-failed': 'Не удалось загрузить фото комнаты. Попробуйте ещё раз.'
6766
+ },
6756
6767
  resourceType: {
6757
6768
  rugs: {
6758
6769
  productCategory: 'Ковер',
@@ -6833,18 +6844,28 @@ roomViewerI18n.use(i18next_browser_languagedetector).use(initReactI18next).init(
6833
6844
  }
6834
6845
  });
6835
6846
  const PROPERTY_BLOCK_RE = /@property\s+(--[^{\s]+)\s*\{([^}]*)\}/g;
6836
- const INITIAL_VALUE_RE = /initial-value:([^;\n]+);/i;
6847
+ const INITIAL_VALUE_RE = /initial-value:([^;}]+)/i;
6848
+ const NON_INHERITING_RE = /inherits:\s*false(?:\s*;|\s*$)/i;
6837
6849
  const ROOT_TO_HOST_SELECTOR_RE = /:root\s*,\s*:host|:root/g;
6838
6850
  function createShadowRootCssText(cssText) {
6839
6851
  const cssForHost = cssText.replace(ROOT_TO_HOST_SELECTOR_RE, ':host');
6840
6852
  const propertyBlocks = Array.from(cssText.matchAll(PROPERTY_BLOCK_RE));
6841
6853
  const propertyDefaults = propertyBlocks.map(([, name, body])=>{
6842
- const match = body.match(INITIAL_VALUE_RE);
6843
- if (!match) return null;
6844
- const initialValue = match[1].trim();
6845
- return ` ${name}: ${initialValue};`;
6854
+ if (!NON_INHERITING_RE.test(body)) return null;
6855
+ const initialValue = body.match(INITIAL_VALUE_RE)?.[1].trim() ?? 'initial';
6856
+ return ` ${name}: ${initialValue};`;
6846
6857
  }).filter((value)=>null !== value).join('\n');
6847
- return propertyDefaults ? `:host {\n${propertyDefaults}\n}\n\n${cssForHost}` : cssForHost;
6858
+ if (!propertyDefaults) return cssForHost;
6859
+ return `${cssForHost}\n\n@layer base {
6860
+ :host,
6861
+ *,
6862
+ ::before,
6863
+ ::after,
6864
+ ::backdrop,
6865
+ ::file-selector-button {
6866
+ ${propertyDefaults}
6867
+ }
6868
+ }`;
6848
6869
  }
6849
6870
  var styleinline = __webpack_require__("./src/style.css?inline");
6850
6871
  const shadowRootCssText = createShadowRootCssText(styleinline.A);
@@ -8392,7 +8413,7 @@ function progress_Progress({ className, value, max = 100, ...props }) {
8392
8413
  })
8393
8414
  });
8394
8415
  }
8395
- const fullscreenDialogContentClassName = 'inset-0 h-svh min-w-full max-w-none translate-x-0 translate-y-0 rounded-none border-none bg-transparent p-0 shadow-none sm:p-0 sm:max-w-none';
8416
+ const fullscreenDialogContentClassName = 'inset-0 h-dvh min-w-full max-w-none translate-x-0 translate-y-0 rounded-none border-none bg-transparent p-0 shadow-none sm:p-0 sm:max-w-none';
8396
8417
  const RoomViewerMetadataContext = createContext(null);
8397
8418
  const RoomViewerSurfaceContext = createContext(null);
8398
8419
  const RoomViewerRoomsContext = createContext(null);
@@ -8449,6 +8470,7 @@ const device_isMobile = 'mobile' === device.type;
8449
8470
  function getErrorMessageKey(variant) {
8450
8471
  if ('custom-interior-processing' === variant) return 'floor.error.processingBody';
8451
8472
  if ('viewer-load-failed' === variant) return 'floor.error.loadBody';
8473
+ if ('artwork-load-failed' === variant || 'room-load-failed' === variant) return `loadError.${variant}`;
8452
8474
  return `accessError.${variant}`;
8453
8475
  }
8454
8476
  function FloorErrorDialog({ variant, open, portalContainer, showDialogCloseButton = false, onOpenChange, onTryAgain, onCancel }) {
@@ -10057,6 +10079,23 @@ function CanvasCaptureBridge({ ref }) {
10057
10079
  ]);
10058
10080
  return null;
10059
10081
  }
10082
+ class SceneErrorBoundary extends Component {
10083
+ state = {
10084
+ hasFailed: false
10085
+ };
10086
+ static getDerivedStateFromError() {
10087
+ return {
10088
+ hasFailed: true
10089
+ };
10090
+ }
10091
+ componentDidCatch(error, errorInfo) {
10092
+ console.error('Room viewer scene failed to render', error, errorInfo.componentStack);
10093
+ this.props.onError(error);
10094
+ }
10095
+ render() {
10096
+ return this.state.hasFailed ? null : this.props.children;
10097
+ }
10098
+ }
10060
10099
  const RESOURCE_BASE_NORMAL = new Vector3(0, 0, 1);
10061
10100
  const WORLD_UP = new Vector3(0, 0, 1);
10062
10101
  const MIN_PROJECTED_UP_LENGTH = 1e-6;
@@ -10997,7 +11036,7 @@ function WallResource({ src, position, quaternion, materialPatch }) {
10997
11036
  }
10998
11037
  function Room() {
10999
11038
  const { placement } = useRoomViewerMetadata();
11000
- const { src, glbUrl } = useRoomViewerResource();
11039
+ const { src, glbUrl, setLoadError } = useRoomViewerResource();
11001
11040
  const { room } = useRoomViewerRooms();
11002
11041
  const { patchSceneLoad } = useRoomViewerSceneState();
11003
11042
  const { gl, size, camera, invalidate, raycaster, pointer } = useThree();
@@ -11099,16 +11138,19 @@ function Room() {
11099
11138
  quaternion: scenePlacement.resourcePose.quaternion,
11100
11139
  materialPatch: materialPatch
11101
11140
  })
11102
- }) : scenePlacement.resourcePose && src && /*#__PURE__*/ jsx_runtime_jsx(DragControls, {
11103
- autoTransform: false,
11104
- onDrag: scenePlacement.handleDrag,
11105
- onDragEnd: scenePlacement.handleDragEnd,
11106
- onDragStart: scenePlacement.handleDragStart,
11107
- children: /*#__PURE__*/ jsx_runtime_jsx(WallResource, {
11108
- src: src,
11109
- position: scenePlacement.resourcePose.position,
11110
- quaternion: scenePlacement.resourcePose.quaternion,
11111
- materialPatch: materialPatch
11141
+ }) : scenePlacement.resourcePose && src && /*#__PURE__*/ jsx_runtime_jsx(SceneErrorBoundary, {
11142
+ onError: ()=>setLoadError('artwork-load-failed'),
11143
+ children: /*#__PURE__*/ jsx_runtime_jsx(DragControls, {
11144
+ autoTransform: false,
11145
+ onDrag: scenePlacement.handleDrag,
11146
+ onDragEnd: scenePlacement.handleDragEnd,
11147
+ onDragStart: scenePlacement.handleDragStart,
11148
+ children: /*#__PURE__*/ jsx_runtime_jsx(WallResource, {
11149
+ src: src,
11150
+ position: scenePlacement.resourcePose.position,
11151
+ quaternion: scenePlacement.resourcePose.quaternion,
11152
+ materialPatch: materialPatch
11153
+ })
11112
11154
  })
11113
11155
  }),
11114
11156
  scene.planes.map((plane)=>/*#__PURE__*/ jsx_runtime_jsx(RoomPlane, {
@@ -11116,7 +11158,53 @@ function Room() {
11116
11158
  geometry: scenePlacement.planeGeometries.get(plane.id),
11117
11159
  setPlaneRef: scenePlacement.setPlaneRef
11118
11160
  }, plane.id)),
11119
- /*#__PURE__*/ jsx_runtime_jsx(RoomBackground, {})
11161
+ /*#__PURE__*/ jsx_runtime_jsx(SceneErrorBoundary, {
11162
+ onError: ()=>setLoadError('room-load-failed'),
11163
+ children: /*#__PURE__*/ jsx_runtime_jsx(RoomBackground, {})
11164
+ })
11165
+ ]
11166
+ });
11167
+ }
11168
+ async function layoutGetConfig(data, config = {}) {
11169
+ const { client: request = client_client, ...requestConfig } = config;
11170
+ const requestData = data;
11171
+ const res = await request({
11172
+ method: "POST",
11173
+ url: "/layout/GetConfig",
11174
+ data: requestData,
11175
+ ...requestConfig
11176
+ });
11177
+ return res.data;
11178
+ }
11179
+ const HDR_NEUTRAL_LINK_TYPE = 'hdr_neutral';
11180
+ const ROOM_VIEWER_CONFIG_QUERY_KEY = [
11181
+ 'room-viewer',
11182
+ 'layout-config'
11183
+ ];
11184
+ function getNeutralHdrUrl(config) {
11185
+ const url = config?.internal?.links?.find((link)=>link.type === HDR_NEUTRAL_LINK_TYPE)?.url?.trim();
11186
+ return url || void 0;
11187
+ }
11188
+ function FloorRoomEnvironment() {
11189
+ const configQuery = react_query_useQuery({
11190
+ queryKey: ROOM_VIEWER_CONFIG_QUERY_KEY,
11191
+ queryFn: ()=>layoutGetConfig(),
11192
+ staleTime: 1 / 0
11193
+ });
11194
+ if (configQuery.isPending) return null;
11195
+ const hdrUrl = getNeutralHdrUrl(configQuery.data);
11196
+ return /*#__PURE__*/ jsx_runtime_jsx(Environment, {
11197
+ files: hdrUrl ? [
11198
+ hdrUrl
11199
+ ] : void 0,
11200
+ preset: hdrUrl ? void 0 : 'warehouse'
11201
+ });
11202
+ }
11203
+ function RoomEnvironment({ placement }) {
11204
+ if ('floor' === placement) return /*#__PURE__*/ jsx_runtime_jsx(FloorRoomEnvironment, {});
11205
+ return /*#__PURE__*/ jsx_runtime_jsx(Environment, {
11206
+ files: [
11207
+ HDR_URL
11120
11208
  ]
11121
11209
  });
11122
11210
  }
@@ -11249,11 +11337,8 @@ function CanvasWrapper({ className, ref, captureRef, onSizeChange, ...props }) {
11249
11337
  /*#__PURE__*/ jsx_runtime_jsx(CanvasCaptureBridge, {
11250
11338
  ref: captureRef
11251
11339
  }),
11252
- /*#__PURE__*/ jsx_runtime_jsx(Environment, {
11253
- files: 'wall' === placement ? [
11254
- HDR_URL
11255
- ] : void 0,
11256
- preset: 'floor' === placement ? 'warehouse' : void 0
11340
+ /*#__PURE__*/ jsx_runtime_jsx(RoomEnvironment, {
11341
+ placement: placement
11257
11342
  }),
11258
11343
  /*#__PURE__*/ jsx_runtime_jsx(Room, {})
11259
11344
  ]
@@ -14001,6 +14086,10 @@ function Header({ className, ...props }) {
14001
14086
  })
14002
14087
  });
14003
14088
  }
14089
+ function RoomViewerErrorState_getErrorMessageKey(variant) {
14090
+ if ('viewer-load-failed' === variant || 'artwork-load-failed' === variant || 'room-load-failed' === variant) return `loadError.${variant}`;
14091
+ return `accessError.${variant}`;
14092
+ }
14004
14093
  function RoomViewerErrorState({ variant, showDialogCloseButton = false }) {
14005
14094
  const { t } = useTranslation();
14006
14095
  return /*#__PURE__*/ jsxs("div", {
@@ -14011,7 +14100,7 @@ function RoomViewerErrorState({ variant, showDialogCloseButton = false }) {
14011
14100
  }),
14012
14101
  /*#__PURE__*/ jsx_runtime_jsx("h2", {
14013
14102
  className: "typo-base-16 font-medium text-text-dark",
14014
- children: t(`accessError.${variant}`)
14103
+ children: t(RoomViewerErrorState_getErrorMessageKey(variant))
14015
14104
  }),
14016
14105
  showDialogCloseButton && /*#__PURE__*/ jsx_runtime_jsx(DialogClose, {
14017
14106
  asChild: true,
@@ -14025,18 +14114,19 @@ function RoomViewerErrorState({ variant, showDialogCloseButton = false }) {
14025
14114
  }
14026
14115
  function RoomViewerContainer({ showDialogCloseButton = false }) {
14027
14116
  const { placement } = useRoomViewerMetadata();
14028
- const { isViewerAccessPending, accessError, hasRooms } = useRoomViewerResource();
14117
+ const { isViewerAccessPending, accessError, loadError, setLoadError, hasRooms } = useRoomViewerResource();
14029
14118
  const { setContainerRef, sceneLoad, isRoomScenePending } = useRoomViewerSceneState();
14030
14119
  const { t } = useTranslation();
14031
14120
  const isSceneLoading = isViewerAccessPending || isRoomScenePending || hasRooms && !Object.values(sceneLoad).every(Boolean);
14032
14121
  if ('floor' === placement) return /*#__PURE__*/ jsx_runtime_jsx(FloorViewerLayout, {
14033
14122
  showDialogCloseButton: showDialogCloseButton
14034
14123
  });
14035
- if (accessError) return /*#__PURE__*/ jsx_runtime_jsx("div", {
14124
+ const activeError = accessError ?? loadError;
14125
+ if (activeError) return /*#__PURE__*/ jsx_runtime_jsx("div", {
14036
14126
  ref: setContainerRef,
14037
14127
  className: "relative size-full overflow-hidden bg-radial from-background-containers to-background-base-2 text-foreground",
14038
14128
  children: /*#__PURE__*/ jsx_runtime_jsx(RoomViewerErrorState, {
14039
- variant: accessError,
14129
+ variant: activeError,
14040
14130
  showDialogCloseButton: showDialogCloseButton
14041
14131
  })
14042
14132
  });
@@ -14063,8 +14153,11 @@ function RoomViewerContainer({ showDialogCloseButton = false }) {
14063
14153
  /*#__PURE__*/ jsxs("div", {
14064
14154
  className: "size-full max-w-full grid grid-rows-[1fr_auto]",
14065
14155
  children: [
14066
- /*#__PURE__*/ jsx_runtime_jsx(Scene, {
14067
- className: "size-full min-h-0"
14156
+ /*#__PURE__*/ jsx_runtime_jsx(SceneErrorBoundary, {
14157
+ onError: ()=>setLoadError('viewer-load-failed'),
14158
+ children: /*#__PURE__*/ jsx_runtime_jsx(Scene, {
14159
+ className: "size-full min-h-0"
14160
+ })
14068
14161
  }),
14069
14162
  /*#__PURE__*/ jsx_runtime_jsx(Footer, {
14070
14163
  className: "bg-background"
@@ -14100,6 +14193,9 @@ function useFrameWidthOptions() {
14100
14193
  t
14101
14194
  ]);
14102
14195
  }
14196
+ function isBlankArtworkSource(src) {
14197
+ return 'string' != typeof src || 0 === src.trim().length;
14198
+ }
14103
14199
  const LEGACY_RESOURCE_TYPE_TO_CANONICAL = {
14104
14200
  poster: 'wallart',
14105
14201
  carpet: 'rugs'
@@ -14308,8 +14404,9 @@ function RoomViewerProvider({ children, src, tinuuid: initialTinuuid, type, view
14308
14404
  const isViewerAccessPending = shouldActivatePublicApi && activationQuery.isLoading || isRoomsAccessPending;
14309
14405
  const activationLoadError = activationQuery.error && null === activationAccessError ? 'viewer-load-failed' : null;
14310
14406
  const objectLoadError = objectQuery.error && null === objectAccessError ? 'viewer-load-failed' : null;
14407
+ const artworkLoadError = 'wall' === placement && isBlankArtworkSource(src) ? 'artwork-load-failed' : null;
14311
14408
  const viewerAccessError = accessError ?? activationAccessError ?? roomsAccessError ?? objectAccessError;
14312
- const viewerLoadError = loadError ?? activationLoadError ?? objectLoadError;
14409
+ const viewerLoadError = loadError ?? activationLoadError ?? objectLoadError ?? artworkLoadError;
14313
14410
  const patchSceneLoad = useCallback((patch)=>{
14314
14411
  setSceneLoad((current)=>Object.assign({}, current, patch));
14315
14412
  }, []);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@neurodyn/react-room-viewer",
3
3
  "type": "module",
4
- "version": "0.0.12",
4
+ "version": "0.0.14",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -46,7 +46,7 @@
46
46
  "react-zoom-pan-pinch": "3.7.0",
47
47
  "swiper": "^12.1.0",
48
48
  "tailwind-merge": "3.4.0",
49
- "three": "^0.182.0",
49
+ "three": "^0.185.1",
50
50
  "ua-parser-js": "^2.0.9"
51
51
  },
52
52
  "devDependencies": {
@@ -58,7 +58,7 @@
58
58
  "@tailwindcss/postcss": "4.1.17",
59
59
  "@types/react": "^19.2.4",
60
60
  "@types/react-dom": "19.2.3",
61
- "@types/three": "0.182.0",
61
+ "@types/three": "0.185.4",
62
62
  "eslint": "9.39.1",
63
63
  "postcss-rem-to-responsive-pixel": "^7.0.4",
64
64
  "react": "^19.2.0",
@@ -70,7 +70,8 @@
70
70
  "scripts": {
71
71
  "dev": "rsbuild dev",
72
72
  "build": "rslib build",
73
- "test": "vitest run",
73
+ "test": "vitest run src",
74
+ "test:e2e": "playwright test e2e",
74
75
  "typecheck": "tsc --noEmit",
75
76
  "lint": "eslint .",
76
77
  "lint:fix": "eslint . --fix",