@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.
Files changed (30) hide show
  1. package/.claude/settings.local.json +1 -1
  2. package/DEVELOPMENT.md +6 -3
  3. package/EXAMPLES.md +193 -153
  4. package/README.md +7 -6
  5. package/SETUP_COMPLETE.md +10 -8
  6. package/dist/components/SkinLayer/HotspotCategorySlideIn/ProductSlideIn/index.d.ts.map +1 -1
  7. package/dist/components/SkinLayer/SearchAndDiscoverySlideIn/HelpActionPart.d.ts.map +1 -1
  8. package/dist/components/SkinLayer/SearchAndDiscoverySlideIn/PoiInfoActionPart/index.d.ts.map +1 -1
  9. package/dist/components/SkinLayer/SearchAndDiscoverySlideIn/ScenariosPart/index.d.ts.map +1 -1
  10. package/dist/constants/SkinLayer/customLayoutPaths.d.ts.map +1 -1
  11. package/dist/hooks/SkinLayer/useProductShake.d.ts +15 -0
  12. package/dist/hooks/SkinLayer/useProductShake.d.ts.map +1 -0
  13. package/dist/hooks/headless/useScenarioControl.d.ts.map +1 -1
  14. package/dist/hooks/useToolConfig.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/models/Visualizer/Tour.d.ts +7 -0
  17. package/dist/models/Visualizer/Tour.d.ts.map +1 -1
  18. package/dist/models/Visualizer/TourScenario/TourScenarioPlayer.d.ts.map +1 -1
  19. package/dist/types/SkinLayer/tool.type.d.ts +4 -0
  20. package/dist/types/SkinLayer/tool.type.d.ts.map +1 -1
  21. package/dist/types/custom-layout.d.ts.map +1 -1
  22. package/dist/web.js +1 -1
  23. package/example/CSS_HANDLING.md +4 -4
  24. package/example/FIXES_SUMMARY.md +18 -8
  25. package/example/PATH_ALIASES.md +13 -14
  26. package/example/README.md +0 -1
  27. package/example/index.html +0 -1
  28. package/example/postcss.config.cjs +1 -1
  29. package/example/tsconfig.node.json +0 -1
  30. package/package.json +1 -1
@@ -16,4 +16,4 @@
16
16
  ],
17
17
  "deny": []
18
18
  }
19
- }
19
+ }
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>> = (props) => {
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 .minimap__marker:not(.minimap__marker--active) .marker__content {
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>> = (props) => {
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 className="dashboard" style={{ padding: '20px', background: '#f0f0f0' }}>
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>Current Scene: <strong>{controls.activeScene?.name}</strong></p>
560
- <p>Total Scenes: <strong>{controls.navigation.totalScenes}</strong></p>
561
- <p>Sound: <strong>{controls.viewport.tourSoundPlaying ? 'ON' : 'OFF'}</strong></p>
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
- Fullscreen
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 style={{
618
- position: 'absolute',
619
- bottom: 20,
620
- left: '50%',
621
- transform: 'translateX(-50%)',
622
- background: 'rgba(0,0,0,0.8)',
623
- color: 'white',
624
- padding: '15px 30px',
625
- borderRadius: '30px',
626
- display: 'flex',
627
- alignItems: 'center',
628
- gap: '15px'
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 && ` of ${scenario.activeScenario.actions.length}`}
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 { ShowroomVisualizer, usePOIInteraction, useTourCore } from 'showroom-visualizer';
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 style={{
673
- position: 'absolute',
674
- right: 20,
675
- top: 20,
676
- width: '300px',
677
- background: 'white',
678
- borderRadius: '10px',
679
- padding: '15px',
680
- maxHeight: '500px',
681
- overflowY: 'auto'
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: poi.activePoiCode === poiItem.code ? '#e3f2fd' : '#f5f5f5',
719
+ background:
720
+ poi.activePoiCode === poiItem.code ? '#e3f2fd' : '#f5f5f5',
698
721
  borderRadius: '5px',
699
722
  cursor: 'pointer',
700
- border: poi.activePoiCode === poiItem.code ? '2px solid #2196F3' : 'none'
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 style={{
736
- position: 'absolute',
737
- top: 0,
738
- left: 0,
739
- right: 0,
740
- height: '60px',
741
- background: 'rgba(0,0,0,0.8)',
742
- color: 'white',
743
- display: 'flex',
744
- alignItems: 'center',
745
- padding: '0 20px',
746
- zIndex: 1000
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 style={{
764
- position: 'absolute',
765
- bottom: 0,
766
- left: 0,
767
- right: 0,
768
- height: '80px',
769
- background: 'rgba(0,0,0,0.8)',
770
- color: 'white',
771
- display: 'flex',
772
- alignItems: 'center',
773
- padding: '0 20px',
774
- gap: '20px',
775
- zIndex: 1000
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 {controls.navigation.scenes.findIndex(s => s.id === controls.activeScene?.id) + 1}
786
- {' '} of {controls.navigation.totalScenes}
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 style={{
799
- position: 'absolute',
800
- right: 20,
801
- top: '50%',
802
- transform: 'translateY(-50%)',
803
- zIndex: 1000
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 style={{
811
- position: 'absolute',
812
- left: 20,
813
- bottom: 100,
814
- width: '250px',
815
- zIndex: 1000
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 style={{
870
- position: 'absolute',
871
- top: 0,
872
- right: 0,
873
- width: '80%',
874
- height: '100%',
875
- background: 'white',
876
- zIndex: 1500,
877
- padding: '20px',
878
- overflowY: 'auto',
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: scene.id === controls.activeScene?.id ? '#007bff' : '#f5f5f5',
896
- color: scene.id === controls.activeScene?.id ? 'white' : 'black',
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 style={{
910
- position: 'absolute',
911
- bottom: 0,
912
- left: 0,
913
- right: 0,
914
- height: '60px',
915
- background: 'rgba(0,0,0,0.7)',
916
- color: 'white',
917
- display: 'flex',
918
- alignItems: 'center',
919
- justifyContent: 'space-between',
920
- padding: '0 20px',
921
- zIndex: 1000
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
-