@clikvn/showroom-visualizer 0.4.1-dev-02 → 0.4.1-dev-04
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/.claude/settings.local.json +1 -1
- package/DEVELOPMENT.md +6 -3
- package/EXAMPLES.md +193 -153
- package/README.md +7 -6
- package/SETUP_COMPLETE.md +10 -8
- package/dist/components/SkinLayer/HotspotCategorySlideIn/ProductSlideIn/index.d.ts.map +1 -1
- package/dist/components/SkinLayer/SearchAndDiscoverySlideIn/HelpActionPart.d.ts.map +1 -1
- package/dist/components/SkinLayer/SearchAndDiscoverySlideIn/PoiInfoActionPart/index.d.ts.map +1 -1
- package/dist/components/SkinLayer/SearchAndDiscoverySlideIn/ScenariosPart/index.d.ts.map +1 -1
- package/dist/constants/SkinLayer/customLayoutPaths.d.ts.map +1 -1
- package/dist/hooks/SkinLayer/useProductShake.d.ts +15 -0
- package/dist/hooks/SkinLayer/useProductShake.d.ts.map +1 -0
- package/dist/hooks/headless/useScenarioControl.d.ts.map +1 -1
- package/dist/hooks/useToolConfig.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/models/Visualizer/Tour.d.ts +7 -0
- package/dist/models/Visualizer/Tour.d.ts.map +1 -1
- package/dist/models/Visualizer/TourScenario/TourScenarioPlayer.d.ts.map +1 -1
- package/dist/types/SkinLayer/tool.type.d.ts +4 -0
- package/dist/types/SkinLayer/tool.type.d.ts.map +1 -1
- package/dist/types/custom-layout.d.ts.map +1 -1
- package/dist/web.js +1 -1
- package/example/CSS_HANDLING.md +4 -4
- package/example/FIXES_SUMMARY.md +18 -8
- package/example/PATH_ALIASES.md +13 -14
- package/example/README.md +0 -1
- package/example/index.html +0 -1
- package/example/postcss.config.cjs +1 -1
- package/example/tsconfig.node.json +0 -1
- package/package.json +1 -1
package/DEVELOPMENT.md
CHANGED
|
@@ -18,6 +18,7 @@ showroom-visualizer/
|
|
|
18
18
|
### Workflow 1: Test trong Example App (Khuyến nghị ⭐)
|
|
19
19
|
|
|
20
20
|
**Ưu điểm:**
|
|
21
|
+
|
|
21
22
|
- ✅ Import trực tiếp từ source (`src/`) - không cần build
|
|
22
23
|
- ✅ Hot reload cực nhanh
|
|
23
24
|
- ✅ Debug dễ dàng với source maps
|
|
@@ -39,6 +40,7 @@ yarn dev
|
|
|
39
40
|
```
|
|
40
41
|
|
|
41
42
|
**Modify và test:**
|
|
43
|
+
|
|
42
44
|
- Thay đổi code trong `src/` → Auto reload
|
|
43
45
|
- Thay đổi code trong `example/src/App.tsx` → Auto reload
|
|
44
46
|
- Test custom layout bằng checkbox trong UI
|
|
@@ -68,15 +70,17 @@ Xem file `dist/index.html` để biết cách dùng Web Component.
|
|
|
68
70
|
Thư viện này có **2 build outputs**:
|
|
69
71
|
|
|
70
72
|
### 1. NPM Package (`dist/index.js`)
|
|
73
|
+
|
|
71
74
|
- **Dùng cho:** React/Next.js apps
|
|
72
75
|
- **React:** External (dùng React của project)
|
|
73
76
|
- **Hỗ trợ:** Custom layout overrides, custom components
|
|
74
|
-
- **Import:**
|
|
77
|
+
- **Import:**
|
|
75
78
|
```tsx
|
|
76
79
|
import { ShowroomVisualizer } from '@clikvn/showroom-visualizer';
|
|
77
80
|
```
|
|
78
81
|
|
|
79
82
|
### 2. Web Component (`dist/web.js`)
|
|
83
|
+
|
|
80
84
|
- **Dùng cho:** Vanilla HTML/JS
|
|
81
85
|
- **React:** Bundled (React 18 đã được bundle sẵn)
|
|
82
86
|
- **Không hỗ trợ:** Custom layout overrides
|
|
@@ -96,7 +100,6 @@ Thư viện này có **2 build outputs**:
|
|
|
96
100
|
2. Test trong `example/` app
|
|
97
101
|
3. Build: `yarn build`
|
|
98
102
|
|
|
99
|
-
|
|
100
103
|
## 🐛 Troubleshooting
|
|
101
104
|
|
|
102
105
|
### Build errors
|
|
@@ -106,6 +109,7 @@ Thư viện này có **2 build outputs**:
|
|
|
106
109
|
rm -rf dist
|
|
107
110
|
yarn build
|
|
108
111
|
```
|
|
112
|
+
|
|
109
113
|
---
|
|
110
114
|
|
|
111
115
|
## ✨ Tips
|
|
@@ -117,4 +121,3 @@ yarn build
|
|
|
117
121
|
---
|
|
118
122
|
|
|
119
123
|
Happy coding! 🚀
|
|
120
|
-
|
package/EXAMPLES.md
CHANGED
|
@@ -48,8 +48,10 @@ Ví dụ này tạo một custom UI với Floorplan có animation và controls
|
|
|
48
48
|
**Approach: Sử dụng `disableDefaultUI` và `useShowroomControls` hook**
|
|
49
49
|
|
|
50
50
|
Khi sử dụng `useShowroomControls`, bạn sẽ nhận được `controls` object chứa:
|
|
51
|
+
|
|
51
52
|
- Tất cả state và functions từ headless hooks (floorplan, navigation, etc.)
|
|
52
53
|
- Tất cả UI components (Floorplan, PinActions, etc.)
|
|
54
|
+
|
|
53
55
|
### Concept
|
|
54
56
|
|
|
55
57
|
```
|
|
@@ -97,6 +99,7 @@ customLayout={{
|
|
|
97
99
|
```
|
|
98
100
|
|
|
99
101
|
**Lý do:**
|
|
102
|
+
|
|
100
103
|
- Custom layout expects **component functions** để có thể render với props động
|
|
101
104
|
- Nếu bạn pass `<MyCustomMarker />`, đó là **React element**, không phải component
|
|
102
105
|
- Code sẽ cố gắng extract component, nhưng **best practice** là pass function trực tiếp
|
|
@@ -251,7 +254,9 @@ import MiniMapMarkerDefault from '@clikvn/showroom-visualizer/dist/components/Sk
|
|
|
251
254
|
import type { ComponentProps } from 'react';
|
|
252
255
|
import './MyMarker.css'; // Import CSS file
|
|
253
256
|
|
|
254
|
-
const MyCustomMarker: FC<ComponentProps<typeof MiniMapMarkerDefault>> = (
|
|
257
|
+
const MyCustomMarker: FC<ComponentProps<typeof MiniMapMarkerDefault>> = (
|
|
258
|
+
props
|
|
259
|
+
) => {
|
|
255
260
|
return (
|
|
256
261
|
<div className="my-marker-wrapper">
|
|
257
262
|
<MiniMapMarkerDefault {...props} />
|
|
@@ -269,12 +274,12 @@ export default MyCustomMarker;
|
|
|
269
274
|
.my-marker-wrapper .minimap__marker {
|
|
270
275
|
border-radius: 50% !important;
|
|
271
276
|
border: 3px solid white !important;
|
|
272
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;
|
|
277
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
|
|
273
278
|
}
|
|
274
279
|
|
|
275
280
|
.my-marker-wrapper .minimap__marker--active {
|
|
276
281
|
transform: scale(1.2) !important;
|
|
277
|
-
box-shadow: 0 0 15px rgba(255,0,0,0.6) !important;
|
|
282
|
+
box-shadow: 0 0 15px rgba(255, 0, 0, 0.6) !important;
|
|
278
283
|
}
|
|
279
284
|
|
|
280
285
|
.my-marker-wrapper .marker__content {
|
|
@@ -286,7 +291,9 @@ export default MyCustomMarker;
|
|
|
286
291
|
background: #ff0000 !important;
|
|
287
292
|
}
|
|
288
293
|
|
|
289
|
-
.my-marker-wrapper.color-only
|
|
294
|
+
.my-marker-wrapper.color-only
|
|
295
|
+
.minimap__marker:not(.minimap__marker--active)
|
|
296
|
+
.marker__content {
|
|
290
297
|
background: #0000ff !important;
|
|
291
298
|
}
|
|
292
299
|
```
|
|
@@ -317,7 +324,9 @@ import ProductDetailDefault from '@clikvn/showroom-visualizer/dist/components/Sk
|
|
|
317
324
|
import type { ComponentProps } from 'react';
|
|
318
325
|
import './MyProductDetail.css';
|
|
319
326
|
|
|
320
|
-
const MyProductDetail: FC<ComponentProps<typeof ProductDetailDefault>> = (
|
|
327
|
+
const MyProductDetail: FC<ComponentProps<typeof ProductDetailDefault>> = (
|
|
328
|
+
props
|
|
329
|
+
) => {
|
|
321
330
|
return (
|
|
322
331
|
<div className="my-product-detail-wrapper">
|
|
323
332
|
<ProductDetailDefault {...props} />
|
|
@@ -340,24 +349,24 @@ export default MyProductDetail;
|
|
|
340
349
|
@apply [&_.text-\\[18px\\]]:text-2xl;
|
|
341
350
|
@apply [&_.text-\\[18px\\]]:font-bold;
|
|
342
351
|
@apply [&_.text-card-foreground]:text-blue-600;
|
|
343
|
-
|
|
352
|
+
|
|
344
353
|
/* Override price styles */
|
|
345
354
|
@apply [&_.text-\\[16px\\]]:text-lg;
|
|
346
355
|
@apply [&_.text-primary]:text-red-500;
|
|
347
356
|
@apply [&_.text-muted-foreground]:text-gray-400;
|
|
348
|
-
|
|
357
|
+
|
|
349
358
|
/* Override button styles */
|
|
350
359
|
@apply [&_button]:rounded-lg;
|
|
351
360
|
@apply [&_button]:transition-colors;
|
|
352
361
|
@apply [&_button:hover]:bg-gray-100;
|
|
353
362
|
}
|
|
354
|
-
|
|
363
|
+
|
|
355
364
|
/* Variant: chỉ thay đổi màu sắc */
|
|
356
365
|
.my-product-detail-wrapper.color-only {
|
|
357
366
|
@apply [&_.text-card-foreground]:text-purple-600;
|
|
358
367
|
@apply [&_.text-primary]:text-orange-500;
|
|
359
368
|
}
|
|
360
|
-
|
|
369
|
+
|
|
361
370
|
/* Variant: chỉ thay đổi font */
|
|
362
371
|
.my-product-detail-wrapper.font-only {
|
|
363
372
|
@apply [&_.text-\\[18px\\]]:text-xl;
|
|
@@ -404,6 +413,7 @@ export default MyProductDetail;
|
|
|
404
413
|
```
|
|
405
414
|
|
|
406
415
|
**Lưu ý quan trọng:**
|
|
416
|
+
|
|
407
417
|
- Escape brackets trong CSS: `.text-\[18px\]` (không phải `.text-[18px]`)
|
|
408
418
|
- Có thể cần `!important` để override Tailwind classes
|
|
409
419
|
- Dùng `[&_selector]` trong Tailwind để target nested elements
|
|
@@ -538,47 +548,50 @@ Floorplan
|
|
|
538
548
|
|
|
539
549
|
```tsx
|
|
540
550
|
import { useState } from 'react';
|
|
541
|
-
import {
|
|
542
|
-
ShowroomVisualizer,
|
|
543
|
-
useShowroomControls
|
|
544
|
-
} from 'showroom-visualizer';
|
|
551
|
+
import { ShowroomVisualizer, useShowroomControls } from 'showroom-visualizer';
|
|
545
552
|
|
|
546
553
|
function ExternalDashboard() {
|
|
547
554
|
const controls = useShowroomControls();
|
|
548
|
-
|
|
555
|
+
|
|
549
556
|
if (!controls.tourReady) {
|
|
550
557
|
return <div>Loading tour...</div>;
|
|
551
558
|
}
|
|
552
|
-
|
|
559
|
+
|
|
553
560
|
return (
|
|
554
|
-
<div
|
|
561
|
+
<div
|
|
562
|
+
className="dashboard"
|
|
563
|
+
style={{ padding: '20px', background: '#f0f0f0' }}
|
|
564
|
+
>
|
|
555
565
|
<h1>Tour Dashboard</h1>
|
|
556
|
-
|
|
566
|
+
|
|
557
567
|
{/* Tour Info */}
|
|
558
568
|
<div className="info-panel">
|
|
559
|
-
<p>
|
|
560
|
-
|
|
561
|
-
|
|
569
|
+
<p>
|
|
570
|
+
Current Scene: <strong>{controls.activeScene?.name}</strong>
|
|
571
|
+
</p>
|
|
572
|
+
<p>
|
|
573
|
+
Total Scenes: <strong>{controls.navigation.totalScenes}</strong>
|
|
574
|
+
</p>
|
|
575
|
+
<p>
|
|
576
|
+
Sound:{' '}
|
|
577
|
+
<strong>{controls.viewport.tourSoundPlaying ? 'ON' : 'OFF'}</strong>
|
|
578
|
+
</p>
|
|
562
579
|
</div>
|
|
563
|
-
|
|
580
|
+
|
|
564
581
|
{/* Quick Actions */}
|
|
565
582
|
<div className="actions" style={{ marginTop: '20px' }}>
|
|
566
583
|
<button onClick={controls.toggleSound}>
|
|
567
584
|
{controls.viewport.tourSoundPlaying ? 'Mute' : 'Unmute'}
|
|
568
585
|
</button>
|
|
569
|
-
<button onClick={controls.toggleFullscreen}>
|
|
570
|
-
|
|
571
|
-
</button>
|
|
572
|
-
<button onClick={controls.viewport.takeScreenshot}>
|
|
573
|
-
Screenshot
|
|
574
|
-
</button>
|
|
586
|
+
<button onClick={controls.toggleFullscreen}>Fullscreen</button>
|
|
587
|
+
<button onClick={controls.viewport.takeScreenshot}>Screenshot</button>
|
|
575
588
|
</div>
|
|
576
|
-
|
|
589
|
+
|
|
577
590
|
{/* Scene Selector */}
|
|
578
591
|
<div style={{ marginTop: '20px' }}>
|
|
579
592
|
<label>Jump to Scene: </label>
|
|
580
593
|
<select onChange={(e) => controls.goToScene(e.target.value)}>
|
|
581
|
-
{controls.navigation.scenes.map(scene => (
|
|
594
|
+
{controls.navigation.scenes.map((scene) => (
|
|
582
595
|
<option key={scene.id} value={scene.id}>
|
|
583
596
|
{scene.name}
|
|
584
597
|
</option>
|
|
@@ -594,7 +607,7 @@ export default function App() {
|
|
|
594
607
|
<div style={{ display: 'grid', gridTemplateColumns: '300px 1fr' }}>
|
|
595
608
|
{/* External Dashboard */}
|
|
596
609
|
<ExternalDashboard />
|
|
597
|
-
|
|
610
|
+
|
|
598
611
|
{/* Tour without UI */}
|
|
599
612
|
<ShowroomVisualizer
|
|
600
613
|
config={{ tourCode: 'my-tour' }}
|
|
@@ -612,21 +625,23 @@ import { ShowroomVisualizer, useShowroomControls } from 'showroom-visualizer';
|
|
|
612
625
|
|
|
613
626
|
function AutoPlayController() {
|
|
614
627
|
const scenario = useScenarioControl();
|
|
615
|
-
|
|
628
|
+
|
|
616
629
|
return (
|
|
617
|
-
<div
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
+
<div
|
|
631
|
+
style={{
|
|
632
|
+
position: 'absolute',
|
|
633
|
+
bottom: 20,
|
|
634
|
+
left: '50%',
|
|
635
|
+
transform: 'translateX(-50%)',
|
|
636
|
+
background: 'rgba(0,0,0,0.8)',
|
|
637
|
+
color: 'white',
|
|
638
|
+
padding: '15px 30px',
|
|
639
|
+
borderRadius: '30px',
|
|
640
|
+
display: 'flex',
|
|
641
|
+
alignItems: 'center',
|
|
642
|
+
gap: '15px',
|
|
643
|
+
}}
|
|
644
|
+
>
|
|
630
645
|
{!scenario.isPlaying ? (
|
|
631
646
|
<button
|
|
632
647
|
onClick={() => scenario.playScenario('intro')}
|
|
@@ -647,7 +662,8 @@ function AutoPlayController() {
|
|
|
647
662
|
<button onClick={scenario.stopScenario}>⏹</button>
|
|
648
663
|
<div>
|
|
649
664
|
Step {scenario.scenarioCurrentStep?.step || 0}
|
|
650
|
-
{scenario.activeScenario &&
|
|
665
|
+
{scenario.activeScenario &&
|
|
666
|
+
` of ${scenario.activeScenario.actions.length}`}
|
|
651
667
|
</div>
|
|
652
668
|
</>
|
|
653
669
|
)}
|
|
@@ -659,45 +675,55 @@ function AutoPlayController() {
|
|
|
659
675
|
### Example 7: POI Explorer
|
|
660
676
|
|
|
661
677
|
```tsx
|
|
662
|
-
import {
|
|
678
|
+
import {
|
|
679
|
+
ShowroomVisualizer,
|
|
680
|
+
usePOIInteraction,
|
|
681
|
+
useTourCore,
|
|
682
|
+
} from 'showroom-visualizer';
|
|
663
683
|
|
|
664
684
|
function POIExplorer() {
|
|
665
685
|
const poi = usePOIInteraction();
|
|
666
686
|
const tour = useTourCore();
|
|
667
|
-
|
|
687
|
+
|
|
668
688
|
// Get all POIs from current scene
|
|
669
689
|
const currentScenePOIs = tour.activeScene?.pois || [];
|
|
670
|
-
|
|
690
|
+
|
|
671
691
|
return (
|
|
672
|
-
<div
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
692
|
+
<div
|
|
693
|
+
style={{
|
|
694
|
+
position: 'absolute',
|
|
695
|
+
right: 20,
|
|
696
|
+
top: 20,
|
|
697
|
+
width: '300px',
|
|
698
|
+
background: 'white',
|
|
699
|
+
borderRadius: '10px',
|
|
700
|
+
padding: '15px',
|
|
701
|
+
maxHeight: '500px',
|
|
702
|
+
overflowY: 'auto',
|
|
703
|
+
}}
|
|
704
|
+
>
|
|
683
705
|
<h3>Points of Interest</h3>
|
|
684
|
-
|
|
706
|
+
|
|
685
707
|
<button onClick={poi.toggleLabels} style={{ marginBottom: '10px' }}>
|
|
686
708
|
{poi.labelVisible ? 'Hide' : 'Show'} All Labels
|
|
687
709
|
</button>
|
|
688
|
-
|
|
710
|
+
|
|
689
711
|
<ul style={{ listStyle: 'none', padding: 0 }}>
|
|
690
|
-
{currentScenePOIs.map(poiItem => (
|
|
712
|
+
{currentScenePOIs.map((poiItem) => (
|
|
691
713
|
<li
|
|
692
714
|
key={poiItem.id}
|
|
693
715
|
onClick={() => poi.openPoiDetail(poiItem.code)}
|
|
694
716
|
style={{
|
|
695
717
|
padding: '10px',
|
|
696
718
|
margin: '5px 0',
|
|
697
|
-
background:
|
|
719
|
+
background:
|
|
720
|
+
poi.activePoiCode === poiItem.code ? '#e3f2fd' : '#f5f5f5',
|
|
698
721
|
borderRadius: '5px',
|
|
699
722
|
cursor: 'pointer',
|
|
700
|
-
border:
|
|
723
|
+
border:
|
|
724
|
+
poi.activePoiCode === poiItem.code
|
|
725
|
+
? '2px solid #2196F3'
|
|
726
|
+
: 'none',
|
|
701
727
|
}}
|
|
702
728
|
>
|
|
703
729
|
<strong>{poiItem.name}</strong>
|
|
@@ -719,7 +745,7 @@ function POIExplorer() {
|
|
|
719
745
|
### Example 8: Complete Custom UI với Tất cả Features
|
|
720
746
|
|
|
721
747
|
```tsx
|
|
722
|
-
import {
|
|
748
|
+
import {
|
|
723
749
|
ShowroomVisualizer,
|
|
724
750
|
useShowroomControls,
|
|
725
751
|
Floorplan,
|
|
@@ -728,64 +754,69 @@ import {
|
|
|
728
754
|
|
|
729
755
|
function CompleteCustomUI() {
|
|
730
756
|
const controls = useShowroomControls();
|
|
731
|
-
|
|
757
|
+
|
|
732
758
|
return (
|
|
733
759
|
<>
|
|
734
760
|
{/* Top Bar */}
|
|
735
|
-
<div
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
761
|
+
<div
|
|
762
|
+
style={{
|
|
763
|
+
position: 'absolute',
|
|
764
|
+
top: 0,
|
|
765
|
+
left: 0,
|
|
766
|
+
right: 0,
|
|
767
|
+
height: '60px',
|
|
768
|
+
background: 'rgba(0,0,0,0.8)',
|
|
769
|
+
color: 'white',
|
|
770
|
+
display: 'flex',
|
|
771
|
+
alignItems: 'center',
|
|
772
|
+
padding: '0 20px',
|
|
773
|
+
zIndex: 1000,
|
|
774
|
+
}}
|
|
775
|
+
>
|
|
748
776
|
<h1 style={{ margin: 0, fontSize: '18px' }}>
|
|
749
777
|
{controls.activeScene?.name}
|
|
750
778
|
</h1>
|
|
751
|
-
|
|
779
|
+
|
|
752
780
|
<div style={{ marginLeft: 'auto', display: 'flex', gap: '10px' }}>
|
|
753
781
|
<button onClick={controls.toggleSound}>
|
|
754
782
|
{controls.viewport.tourSoundPlaying ? '🔊' : '🔇'}
|
|
755
783
|
</button>
|
|
756
|
-
<button onClick={controls.toggleFullscreen}>
|
|
757
|
-
⛶
|
|
758
|
-
</button>
|
|
784
|
+
<button onClick={controls.toggleFullscreen}>⛶</button>
|
|
759
785
|
</div>
|
|
760
786
|
</div>
|
|
761
|
-
|
|
787
|
+
|
|
762
788
|
{/* Bottom Navigation */}
|
|
763
|
-
<div
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
789
|
+
<div
|
|
790
|
+
style={{
|
|
791
|
+
position: 'absolute',
|
|
792
|
+
bottom: 0,
|
|
793
|
+
left: 0,
|
|
794
|
+
right: 0,
|
|
795
|
+
height: '80px',
|
|
796
|
+
background: 'rgba(0,0,0,0.8)',
|
|
797
|
+
color: 'white',
|
|
798
|
+
display: 'flex',
|
|
799
|
+
alignItems: 'center',
|
|
800
|
+
padding: '0 20px',
|
|
801
|
+
gap: '20px',
|
|
802
|
+
zIndex: 1000,
|
|
803
|
+
}}
|
|
804
|
+
>
|
|
777
805
|
<button
|
|
778
806
|
onClick={controls.goToPreviousScene}
|
|
779
807
|
disabled={!controls.navigation.hasPreviousScene}
|
|
780
808
|
>
|
|
781
809
|
← Previous
|
|
782
810
|
</button>
|
|
783
|
-
|
|
811
|
+
|
|
784
812
|
<div style={{ flex: 1, textAlign: 'center' }}>
|
|
785
|
-
Scene
|
|
786
|
-
{
|
|
813
|
+
Scene{' '}
|
|
814
|
+
{controls.navigation.scenes.findIndex(
|
|
815
|
+
(s) => s.id === controls.activeScene?.id
|
|
816
|
+
) + 1}{' '}
|
|
817
|
+
of {controls.navigation.totalScenes}
|
|
787
818
|
</div>
|
|
788
|
-
|
|
819
|
+
|
|
789
820
|
<button
|
|
790
821
|
onClick={controls.goToNextScene}
|
|
791
822
|
disabled={!controls.navigation.hasNextScene}
|
|
@@ -793,27 +824,31 @@ function CompleteCustomUI() {
|
|
|
793
824
|
Next →
|
|
794
825
|
</button>
|
|
795
826
|
</div>
|
|
796
|
-
|
|
827
|
+
|
|
797
828
|
{/* Right Sidebar - Pin Actions */}
|
|
798
|
-
<div
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
829
|
+
<div
|
|
830
|
+
style={{
|
|
831
|
+
position: 'absolute',
|
|
832
|
+
right: 20,
|
|
833
|
+
top: '50%',
|
|
834
|
+
transform: 'translateY(-50%)',
|
|
835
|
+
zIndex: 1000,
|
|
836
|
+
}}
|
|
837
|
+
>
|
|
805
838
|
<PinActions />
|
|
806
839
|
</div>
|
|
807
|
-
|
|
840
|
+
|
|
808
841
|
{/* Minimap */}
|
|
809
842
|
{controls.showFloorplan && (
|
|
810
|
-
<div
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
843
|
+
<div
|
|
844
|
+
style={{
|
|
845
|
+
position: 'absolute',
|
|
846
|
+
left: 20,
|
|
847
|
+
bottom: 100,
|
|
848
|
+
width: '250px',
|
|
849
|
+
zIndex: 1000,
|
|
850
|
+
}}
|
|
851
|
+
>
|
|
817
852
|
<Floorplan />
|
|
818
853
|
</div>
|
|
819
854
|
)}
|
|
@@ -843,7 +878,7 @@ import { ShowroomVisualizer, useShowroomControls } from 'showroom-visualizer';
|
|
|
843
878
|
function MobileUI() {
|
|
844
879
|
const controls = useShowroomControls();
|
|
845
880
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
846
|
-
|
|
881
|
+
|
|
847
882
|
return (
|
|
848
883
|
<>
|
|
849
884
|
{/* Hamburger Menu Button */}
|
|
@@ -863,24 +898,26 @@ function MobileUI() {
|
|
|
863
898
|
>
|
|
864
899
|
☰
|
|
865
900
|
</button>
|
|
866
|
-
|
|
901
|
+
|
|
867
902
|
{/* Slide-out Menu */}
|
|
868
903
|
{menuOpen && (
|
|
869
|
-
<div
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
904
|
+
<div
|
|
905
|
+
style={{
|
|
906
|
+
position: 'absolute',
|
|
907
|
+
top: 0,
|
|
908
|
+
right: 0,
|
|
909
|
+
width: '80%',
|
|
910
|
+
height: '100%',
|
|
911
|
+
background: 'white',
|
|
912
|
+
zIndex: 1500,
|
|
913
|
+
padding: '20px',
|
|
914
|
+
overflowY: 'auto',
|
|
915
|
+
}}
|
|
916
|
+
>
|
|
880
917
|
<button onClick={() => setMenuOpen(false)}>✕ Close</button>
|
|
881
|
-
|
|
918
|
+
|
|
882
919
|
<h2>Scenes</h2>
|
|
883
|
-
{controls.navigation.scenes.map(scene => (
|
|
920
|
+
{controls.navigation.scenes.map((scene) => (
|
|
884
921
|
<button
|
|
885
922
|
key={scene.id}
|
|
886
923
|
onClick={() => {
|
|
@@ -892,8 +929,10 @@ function MobileUI() {
|
|
|
892
929
|
width: '100%',
|
|
893
930
|
padding: '15px',
|
|
894
931
|
marginBottom: '10px',
|
|
895
|
-
background:
|
|
896
|
-
|
|
932
|
+
background:
|
|
933
|
+
scene.id === controls.activeScene?.id ? '#007bff' : '#f5f5f5',
|
|
934
|
+
color:
|
|
935
|
+
scene.id === controls.activeScene?.id ? 'white' : 'black',
|
|
897
936
|
border: 'none',
|
|
898
937
|
borderRadius: '5px',
|
|
899
938
|
textAlign: 'left',
|
|
@@ -904,22 +943,24 @@ function MobileUI() {
|
|
|
904
943
|
))}
|
|
905
944
|
</div>
|
|
906
945
|
)}
|
|
907
|
-
|
|
946
|
+
|
|
908
947
|
{/* Bottom Swipe Navigation */}
|
|
909
|
-
<div
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
948
|
+
<div
|
|
949
|
+
style={{
|
|
950
|
+
position: 'absolute',
|
|
951
|
+
bottom: 0,
|
|
952
|
+
left: 0,
|
|
953
|
+
right: 0,
|
|
954
|
+
height: '60px',
|
|
955
|
+
background: 'rgba(0,0,0,0.7)',
|
|
956
|
+
color: 'white',
|
|
957
|
+
display: 'flex',
|
|
958
|
+
alignItems: 'center',
|
|
959
|
+
justifyContent: 'space-between',
|
|
960
|
+
padding: '0 20px',
|
|
961
|
+
zIndex: 1000,
|
|
962
|
+
}}
|
|
963
|
+
>
|
|
923
964
|
<button
|
|
924
965
|
onClick={controls.goToPreviousScene}
|
|
925
966
|
disabled={!controls.navigation.hasPreviousScene}
|
|
@@ -933,9 +974,9 @@ function MobileUI() {
|
|
|
933
974
|
>
|
|
934
975
|
←
|
|
935
976
|
</button>
|
|
936
|
-
|
|
977
|
+
|
|
937
978
|
<span>{controls.activeScene?.name}</span>
|
|
938
|
-
|
|
979
|
+
|
|
939
980
|
<button
|
|
940
981
|
onClick={controls.goToNextScene}
|
|
941
982
|
disabled={!controls.navigation.hasNextScene}
|
|
@@ -964,4 +1005,3 @@ function MobileUI() {
|
|
|
964
1005
|
3. **Error Handling**: Check `tourReady` trước khi render UI
|
|
965
1006
|
4. **Responsive**: Test trên nhiều screen sizes
|
|
966
1007
|
5. **Accessibility**: Thêm ARIA labels cho custom buttons
|
|
967
|
-
|