@clikvn/showroom-visualizer 0.2.2-dev-11 → 0.2.2-dev-13
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 +26 -133
- package/base.json +21 -21
- package/dist/components/SkinLayer/Floorplan/Map.d.ts.map +1 -1
- package/dist/components/SkinLayer/Floorplan/Minimap/MiniMapMarker.d.ts.map +1 -1
- package/dist/components/SkinLayer/Floorplan/Minimap/index.d.ts.map +1 -1
- package/dist/components/SkinLayer/Layout/index.d.ts.map +1 -1
- package/dist/components/SkinLayer/index.d.ts +0 -32
- package/dist/components/SkinLayer/index.d.ts.map +1 -1
- package/dist/features/ShowroomVisualizer/VirtualTour.d.ts +0 -5
- package/dist/features/ShowroomVisualizer/VirtualTour.d.ts.map +1 -1
- package/dist/features/ShowroomVisualizer/VirtualTourContainer.d.ts +3 -4
- package/dist/features/ShowroomVisualizer/VirtualTourContainer.d.ts.map +1 -1
- package/dist/features/ShowroomVisualizer/index.d.ts +3 -24
- package/dist/features/ShowroomVisualizer/index.d.ts.map +1 -1
- package/dist/fonts/icomoon.svg +633 -633
- package/dist/hooks/useToolConfig.d.ts.map +1 -1
- package/dist/index.d.ts +0 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.html +27 -102
- package/dist/models/Visualizer/Tour.d.ts +1 -0
- package/dist/models/Visualizer/Tour.d.ts.map +1 -1
- package/dist/models/Visualizer/TourScenario/TourScenarioPlayer.d.ts +1 -0
- package/dist/models/Visualizer/TourScenario/TourScenarioPlayer.d.ts.map +1 -1
- package/dist/register.d.ts +0 -3
- package/dist/register.d.ts.map +1 -1
- package/dist/types/SkinLayer/tool.type.d.ts +1 -6
- package/dist/types/SkinLayer/tool.type.d.ts.map +1 -1
- package/dist/types/SkinLayer/visualizer.type.d.ts +0 -3
- package/dist/types/SkinLayer/visualizer.type.d.ts.map +1 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +1 -1
- package/package.json +1 -9
- package/rollup.config.js +97 -365
- package/tailwind.config.cjs +151 -151
- package/.idea/inspectionProfiles/Project_Default.xml +0 -36
- package/.idea/jsLinters/eslint.xml +0 -7
- package/.idea/misc.xml +0 -9
- package/.idea/modules.xml +0 -8
- package/.idea/prettier.xml +0 -8
- package/.idea/showroom-visualizer.iml +0 -9
- package/.idea/vcs.xml +0 -6
- package/DEVELOPMENT.md +0 -120
- package/EXAMPLES.md +0 -967
- package/SETUP_COMPLETE.md +0 -149
- package/dist/components/SkinLayer/DefaultLayout/index.d.ts +0 -8
- package/dist/components/SkinLayer/DefaultLayout/index.d.ts.map +0 -1
- package/dist/components/SkinLayer/GalleryFullScreen/Content/ARViewer.d.ts +0 -30
- package/dist/components/SkinLayer/GalleryFullScreen/Content/ARViewer.d.ts.map +0 -1
- package/dist/components/SkinLayer/ModalItemInfo/Description.d.ts +0 -10
- package/dist/components/SkinLayer/ModalItemInfo/Description.d.ts.map +0 -1
- package/dist/components/SkinLayer/ModalItemInfo/Intro.d.ts +0 -9
- package/dist/components/SkinLayer/ModalItemInfo/Intro.d.ts.map +0 -1
- package/dist/components/SkinLayer/ModalItemInfo/Media.d.ts +0 -13
- package/dist/components/SkinLayer/ModalItemInfo/Media.d.ts.map +0 -1
- package/dist/components/SkinLayer/ModalItemInfo/index.d.ts +0 -10
- package/dist/components/SkinLayer/ModalItemInfo/index.d.ts.map +0 -1
- package/dist/components/SkinLayer/PoiTextureOptions/HorizontalMenu/index.d.ts +0 -13
- package/dist/components/SkinLayer/PoiTextureOptions/HorizontalMenu/index.d.ts.map +0 -1
- package/dist/components/SkinLayer/PoiTextureOptions/SemicircleMenu/index.d.ts +0 -13
- package/dist/components/SkinLayer/PoiTextureOptions/SemicircleMenu/index.d.ts.map +0 -1
- package/dist/components/SkinLayer/PoiTextureOptions/TextureMenuItem/index.d.ts +0 -15
- package/dist/components/SkinLayer/PoiTextureOptions/TextureMenuItem/index.d.ts.map +0 -1
- package/dist/components/SkinLayer/PoiTextureOptions/VerticalMenu/index.d.ts +0 -13
- package/dist/components/SkinLayer/PoiTextureOptions/VerticalMenu/index.d.ts.map +0 -1
- package/dist/context/CustomLayoutContext.d.ts +0 -20
- package/dist/context/CustomLayoutContext.d.ts.map +0 -1
- package/dist/context/StoreContext.d.ts +0 -5
- package/dist/context/StoreContext.d.ts.map +0 -1
- package/dist/features/ShowroomVisualizer/Scripts.d.ts +0 -4
- package/dist/features/ShowroomVisualizer/Scripts.d.ts.map +0 -1
- package/dist/features/ShowroomVisualizer/TourContainer.d.ts +0 -9
- package/dist/features/ShowroomVisualizer/TourContainer.d.ts.map +0 -1
- package/dist/features/ShowroomVisualizer/Tours.d.ts +0 -3
- package/dist/features/ShowroomVisualizer/Tours.d.ts.map +0 -1
- package/dist/hooks/Visualizer/reducer.d.ts +0 -116
- package/dist/hooks/Visualizer/reducer.d.ts.map +0 -1
- package/dist/hooks/headless/index.d.ts +0 -150
- package/dist/hooks/headless/index.d.ts.map +0 -1
- package/dist/hooks/headless/useFloorplanControl.d.ts +0 -18
- package/dist/hooks/headless/useFloorplanControl.d.ts.map +0 -1
- package/dist/hooks/headless/usePOIInteraction.d.ts +0 -23
- package/dist/hooks/headless/usePOIInteraction.d.ts.map +0 -1
- package/dist/hooks/headless/useScenarioControl.d.ts +0 -22
- package/dist/hooks/headless/useScenarioControl.d.ts.map +0 -1
- package/dist/hooks/headless/useSceneNavigation.d.ts +0 -26
- package/dist/hooks/headless/useSceneNavigation.d.ts.map +0 -1
- package/dist/hooks/headless/useTourCore.d.ts +0 -23
- package/dist/hooks/headless/useTourCore.d.ts.map +0 -1
- package/dist/hooks/headless/useViewportControl.d.ts +0 -22
- package/dist/hooks/headless/useViewportControl.d.ts.map +0 -1
- package/dist/index.js +0 -1
- package/dist/types/custom-layout.d.ts +0 -63
- package/dist/types/custom-layout.d.ts.map +0 -1
- package/example/CSS_HANDLING.md +0 -141
- package/example/FIXES_SUMMARY.md +0 -121
- package/example/PATH_ALIASES.md +0 -103
- package/example/README.md +0 -64
- package/example/index.html +0 -13
- package/example/package.json +0 -25
- package/example/postcss.config.cjs +0 -6
- package/example/tailwind.config.cjs +0 -12
- package/example/tsconfig.node.json +0 -12
- package/example/vite.config.ts +0 -126
package/EXAMPLES.md
DELETED
|
@@ -1,967 +0,0 @@
|
|
|
1
|
-
# Showroom Visualizer - Examples
|
|
2
|
-
|
|
3
|
-
## 📚 Table of Contents
|
|
4
|
-
|
|
5
|
-
1. [Basic Usage](#basic-usage)
|
|
6
|
-
2. [Custom Layout Examples (Override Components)](#custom-layout-examples-override-components)
|
|
7
|
-
3. [Custom Floorplan Examples](#custom-floorplan-examples)
|
|
8
|
-
4. [Headless Hooks Examples](#headless-hooks-examples)
|
|
9
|
-
5. [Advanced Examples](#advanced-examples)
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Basic Usage
|
|
14
|
-
|
|
15
|
-
### Example 1: Default UI (Không thay đổi)
|
|
16
|
-
|
|
17
|
-
```tsx
|
|
18
|
-
import { ShowroomVisualizer } from 'showroom-visualizer';
|
|
19
|
-
|
|
20
|
-
export default function App() {
|
|
21
|
-
return (
|
|
22
|
-
<ShowroomVisualizer
|
|
23
|
-
config={{
|
|
24
|
-
tourCode: 'my-showroom-2024',
|
|
25
|
-
language: 'vi',
|
|
26
|
-
}}
|
|
27
|
-
mobile={false}
|
|
28
|
-
apiHost="https://api.clik.vn"
|
|
29
|
-
/>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Custom Layout Examples (Override Components)
|
|
37
|
-
|
|
38
|
-
**Custom layout** cho phép bạn override từng component theo cấu trúc object lồng nhau, giúp tùy chỉnh UI mà không cần thay đổi toàn bộ component.
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## Custom Floorplan Examples
|
|
43
|
-
|
|
44
|
-
### Example 5: Basic Custom Floorplan UI
|
|
45
|
-
|
|
46
|
-
Ví dụ này tạo một custom UI với Floorplan có animation và controls đơn giản:
|
|
47
|
-
|
|
48
|
-
**Approach: Sử dụng `disableDefaultUI` và `useShowroomControls` hook**
|
|
49
|
-
|
|
50
|
-
Khi sử dụng `useShowroomControls`, bạn sẽ nhận được `controls` object chứa:
|
|
51
|
-
- Tất cả state và functions từ headless hooks (floorplan, navigation, etc.)
|
|
52
|
-
- Tất cả UI components (Floorplan, PinActions, etc.)
|
|
53
|
-
### Concept
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
Floorplan
|
|
57
|
-
├── Map
|
|
58
|
-
└── Minimap
|
|
59
|
-
├── Marker ← Có thể override riêng!
|
|
60
|
-
└── Polygon ← Có thể override riêng!
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### CustomLayoutConfig type
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
import { CustomLayoutConfig } from '@clikvn/showroom-visualizer';
|
|
67
|
-
|
|
68
|
-
const customLayout: CustomLayoutConfig = {
|
|
69
|
-
Floorplan: {
|
|
70
|
-
Minimap: {
|
|
71
|
-
Marker: MyCustomMarker,
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### ⚠️ IMPORTANT: Pass Component Functions, NOT React Elements!
|
|
78
|
-
|
|
79
|
-
```tsx
|
|
80
|
-
// ✅ ĐÚNG - Pass component function
|
|
81
|
-
customLayout={{
|
|
82
|
-
Floorplan: {
|
|
83
|
-
Minimap: {
|
|
84
|
-
Marker: MyCustomMarker, // ✅ Function reference
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
}}
|
|
88
|
-
|
|
89
|
-
// ❌ SAI - Không pass React element
|
|
90
|
-
customLayout={{
|
|
91
|
-
Floorplan: {
|
|
92
|
-
Minimap: {
|
|
93
|
-
Marker: <MyCustomMarker />, // ❌ JSX element (đã render)
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
}}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
**Lý do:**
|
|
100
|
-
- Custom layout expects **component functions** để có thể render với props động
|
|
101
|
-
- Nếu bạn pass `<MyCustomMarker />`, đó là **React element**, không phải component
|
|
102
|
-
- Code sẽ cố gắng extract component, nhưng **best practice** là pass function trực tiếp
|
|
103
|
-
|
|
104
|
-
### Example 1: Override Marker
|
|
105
|
-
|
|
106
|
-
```tsx
|
|
107
|
-
import { ShowroomVisualizer } from '@clikvn/showroom-visualizer';
|
|
108
|
-
|
|
109
|
-
const MyCustomMarker = (props) => {
|
|
110
|
-
const { marker, active, size, invisible, onClick } = props;
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<div
|
|
114
|
-
style={{
|
|
115
|
-
width: size,
|
|
116
|
-
height: size,
|
|
117
|
-
background: active ? 'red' : 'blue',
|
|
118
|
-
borderRadius: '50%',
|
|
119
|
-
border: '3px solid white',
|
|
120
|
-
cursor: 'pointer',
|
|
121
|
-
opacity: invisible ? 0 : 1,
|
|
122
|
-
display: 'flex',
|
|
123
|
-
alignItems: 'center',
|
|
124
|
-
justifyContent: 'center',
|
|
125
|
-
fontSize: size * 0.5,
|
|
126
|
-
transform: active ? 'scale(1.2)' : 'scale(1)',
|
|
127
|
-
transition: 'all 0.3s ease',
|
|
128
|
-
}}
|
|
129
|
-
onClick={() => onClick(marker)}
|
|
130
|
-
>
|
|
131
|
-
{active ? '📍' : '📌'}
|
|
132
|
-
</div>
|
|
133
|
-
);
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
<ShowroomVisualizer
|
|
137
|
-
config={{ tourCode: 'my-tour' }}
|
|
138
|
-
customLayout={{
|
|
139
|
-
Floorplan: {
|
|
140
|
-
Minimap: {
|
|
141
|
-
Marker: MyCustomMarker,
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
}}
|
|
145
|
-
/>;
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Example 2: Override Minimap Component (Reuse Children)
|
|
149
|
-
|
|
150
|
-
Khi override component cha (như `Minimap`), bạn vẫn có thể reuse các children components mặc định:
|
|
151
|
-
|
|
152
|
-
```tsx
|
|
153
|
-
import {
|
|
154
|
-
ShowroomVisualizer,
|
|
155
|
-
FloorplanMinimapDefault,
|
|
156
|
-
} from '@clikvn/showroom-visualizer';
|
|
157
|
-
import { useState } from 'react';
|
|
158
|
-
|
|
159
|
-
const MyMarker = (props) => (
|
|
160
|
-
<div
|
|
161
|
-
style={{
|
|
162
|
-
width: props.size,
|
|
163
|
-
height: props.size,
|
|
164
|
-
background: props.active ? '#ff0000' : '#0000ff',
|
|
165
|
-
borderRadius: '50%',
|
|
166
|
-
}}
|
|
167
|
-
onClick={() => props.onClick(props.marker)}
|
|
168
|
-
>
|
|
169
|
-
{props.active ? '🔴' : '🔵'}
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
const MyMinimap = (props) => {
|
|
174
|
-
const [showLabels, setShowLabels] = useState(false);
|
|
175
|
-
|
|
176
|
-
return (
|
|
177
|
-
<div style={{ position: 'relative' }}>
|
|
178
|
-
<button
|
|
179
|
-
onClick={() => setShowLabels(!showLabels)}
|
|
180
|
-
style={{
|
|
181
|
-
position: 'absolute',
|
|
182
|
-
top: 10,
|
|
183
|
-
right: 10,
|
|
184
|
-
zIndex: 1000,
|
|
185
|
-
}}
|
|
186
|
-
>
|
|
187
|
-
{showLabels ? 'Hide' : 'Show'} Labels
|
|
188
|
-
</button>
|
|
189
|
-
|
|
190
|
-
{/*
|
|
191
|
-
FloorplanMinimapDefault tự động sử dụng custom Marker từ customLayout
|
|
192
|
-
vì nó sử dụng useCustomLayout() bên trong
|
|
193
|
-
*/}
|
|
194
|
-
<FloorplanMinimapDefault {...props} />
|
|
195
|
-
</div>
|
|
196
|
-
);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
<ShowroomVisualizer
|
|
200
|
-
customLayout={{
|
|
201
|
-
Floorplan: {
|
|
202
|
-
Minimap: {
|
|
203
|
-
// Override Minimap component
|
|
204
|
-
default: MyMinimap,
|
|
205
|
-
// Override Marker (sẽ được MyMinimap sử dụng tự động)
|
|
206
|
-
Marker: MyMarker,
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
}}
|
|
210
|
-
/>;
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
**Lưu ý:** `FloorplanMinimapDefault` tự động sử dụng `useCustomLayout()` để lấy `Marker` và `Polygon` từ config, nên bạn không cần truyền props `MarkerComponent` hay `PolygonsComponent`.
|
|
214
|
-
|
|
215
|
-
### Example 3: TypeScript Support
|
|
216
|
-
|
|
217
|
-
```tsx
|
|
218
|
-
import {
|
|
219
|
-
ShowroomVisualizer,
|
|
220
|
-
CustomLayoutConfig,
|
|
221
|
-
MiniMapMarkerDefault,
|
|
222
|
-
} from '@clikvn/showroom-visualizer';
|
|
223
|
-
|
|
224
|
-
const MyMarker: React.ComponentType<any> = (props) => (
|
|
225
|
-
<div>My Custom Marker for {props.marker.name}</div>
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
const customLayoutConfig: CustomLayoutConfig = {
|
|
229
|
-
Floorplan: {
|
|
230
|
-
Minimap: {
|
|
231
|
-
Marker: MyMarker,
|
|
232
|
-
Polygon: MiniMapMarkerDefault, // Có thể reuse default exports
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
<ShowroomVisualizer
|
|
238
|
-
config={{ tourCode: 'my-tour' }}
|
|
239
|
-
customLayout={customLayoutConfig}
|
|
240
|
-
/>;
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Example 4: Wrapper Component với Custom CSS (Pure CSS)
|
|
244
|
-
|
|
245
|
-
Sử dụng wrapper component để override CSS mà không cần thay đổi logic:
|
|
246
|
-
|
|
247
|
-
```tsx
|
|
248
|
-
// MyCustomMarker.tsx
|
|
249
|
-
import { FC } from 'react';
|
|
250
|
-
import MiniMapMarkerDefault from '@clikvn/showroom-visualizer/dist/components/SkinLayer/Floorplan/Minimap/MiniMapMarker';
|
|
251
|
-
import type { ComponentProps } from 'react';
|
|
252
|
-
import './MyMarker.css'; // Import CSS file
|
|
253
|
-
|
|
254
|
-
const MyCustomMarker: FC<ComponentProps<typeof MiniMapMarkerDefault>> = (props) => {
|
|
255
|
-
return (
|
|
256
|
-
<div className="my-marker-wrapper">
|
|
257
|
-
<MiniMapMarkerDefault {...props} />
|
|
258
|
-
</div>
|
|
259
|
-
);
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
export default MyCustomMarker;
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
```css
|
|
266
|
-
/* MyMarker.css */
|
|
267
|
-
|
|
268
|
-
/* Override marker styles */
|
|
269
|
-
.my-marker-wrapper .minimap__marker {
|
|
270
|
-
border-radius: 50% !important;
|
|
271
|
-
border: 3px solid white !important;
|
|
272
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
.my-marker-wrapper .minimap__marker--active {
|
|
276
|
-
transform: scale(1.2) !important;
|
|
277
|
-
box-shadow: 0 0 15px rgba(255,0,0,0.6) !important;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
.my-marker-wrapper .marker__content {
|
|
281
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/* Variant: chỉ thay đổi màu sắc */
|
|
285
|
-
.my-marker-wrapper.color-only .minimap__marker--active .marker__content {
|
|
286
|
-
background: #ff0000 !important;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
.my-marker-wrapper.color-only .minimap__marker:not(.minimap__marker--active) .marker__content {
|
|
290
|
-
background: #0000ff !important;
|
|
291
|
-
}
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Sử dụng:
|
|
295
|
-
|
|
296
|
-
```tsx
|
|
297
|
-
<ShowroomVisualizer
|
|
298
|
-
config={{ tourCode: 'my-tour' }}
|
|
299
|
-
customLayout={{
|
|
300
|
-
Floorplan: {
|
|
301
|
-
Minimap: {
|
|
302
|
-
Marker: MyCustomMarker,
|
|
303
|
-
},
|
|
304
|
-
},
|
|
305
|
-
}}
|
|
306
|
-
/>
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Example 5: Wrapper Component với Tailwind CSS
|
|
310
|
-
|
|
311
|
-
Sử dụng Tailwind với wrapper component:
|
|
312
|
-
|
|
313
|
-
```tsx
|
|
314
|
-
// MyProductDetail.tsx
|
|
315
|
-
import { FC } from 'react';
|
|
316
|
-
import ProductDetailDefault from '@clikvn/showroom-visualizer/dist/components/SkinLayer/PoiDetailSlideIn/Detail';
|
|
317
|
-
import type { ComponentProps } from 'react';
|
|
318
|
-
import './MyProductDetail.css';
|
|
319
|
-
|
|
320
|
-
const MyProductDetail: FC<ComponentProps<typeof ProductDetailDefault>> = (props) => {
|
|
321
|
-
return (
|
|
322
|
-
<div className="my-product-detail-wrapper">
|
|
323
|
-
<ProductDetailDefault {...props} />
|
|
324
|
-
</div>
|
|
325
|
-
);
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
export default MyProductDetail;
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
```css
|
|
332
|
-
/* MyProductDetail.css */
|
|
333
|
-
@tailwind base;
|
|
334
|
-
@tailwind components;
|
|
335
|
-
@tailwind utilities;
|
|
336
|
-
|
|
337
|
-
@layer components {
|
|
338
|
-
.my-product-detail-wrapper {
|
|
339
|
-
/* Override title styles */
|
|
340
|
-
@apply [&_.text-\\[18px\\]]:text-2xl;
|
|
341
|
-
@apply [&_.text-\\[18px\\]]:font-bold;
|
|
342
|
-
@apply [&_.text-card-foreground]:text-blue-600;
|
|
343
|
-
|
|
344
|
-
/* Override price styles */
|
|
345
|
-
@apply [&_.text-\\[16px\\]]:text-lg;
|
|
346
|
-
@apply [&_.text-primary]:text-red-500;
|
|
347
|
-
@apply [&_.text-muted-foreground]:text-gray-400;
|
|
348
|
-
|
|
349
|
-
/* Override button styles */
|
|
350
|
-
@apply [&_button]:rounded-lg;
|
|
351
|
-
@apply [&_button]:transition-colors;
|
|
352
|
-
@apply [&_button:hover]:bg-gray-100;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/* Variant: chỉ thay đổi màu sắc */
|
|
356
|
-
.my-product-detail-wrapper.color-only {
|
|
357
|
-
@apply [&_.text-card-foreground]:text-purple-600;
|
|
358
|
-
@apply [&_.text-primary]:text-orange-500;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/* Variant: chỉ thay đổi font */
|
|
362
|
-
.my-product-detail-wrapper.font-only {
|
|
363
|
-
@apply [&_.text-\\[18px\\]]:text-xl;
|
|
364
|
-
@apply [&_.text-\\[16px\\]]:text-base;
|
|
365
|
-
@apply [&_.font-semibold]:font-extrabold;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
**Hoặc dùng CSS thuần (không cần @apply):**
|
|
371
|
-
|
|
372
|
-
```css
|
|
373
|
-
/* MyProductDetail.css - Pure CSS approach */
|
|
374
|
-
|
|
375
|
-
/* Override title styles */
|
|
376
|
-
.my-product-detail-wrapper .text-\[18px\] {
|
|
377
|
-
font-size: 1.5rem !important; /* text-2xl */
|
|
378
|
-
font-weight: 700 !important; /* font-bold */
|
|
379
|
-
color: rgb(37 99 235) !important; /* blue-600 */
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/* Override price styles */
|
|
383
|
-
.my-product-detail-wrapper .text-\[16px\] {
|
|
384
|
-
font-size: 1.125rem !important; /* text-lg */
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
.my-product-detail-wrapper .text-primary {
|
|
388
|
-
color: rgb(239 68 68) !important; /* red-500 */
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
.my-product-detail-wrapper .text-muted-foreground {
|
|
392
|
-
color: rgb(156 163 175) !important; /* gray-400 */
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/* Override button styles */
|
|
396
|
-
.my-product-detail-wrapper button {
|
|
397
|
-
border-radius: 0.5rem !important; /* rounded-lg */
|
|
398
|
-
transition: all 0.2s !important;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.my-product-detail-wrapper button:hover {
|
|
402
|
-
background-color: rgb(243 244 246) !important; /* bg-gray-100 */
|
|
403
|
-
}
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
**Lưu ý quan trọng:**
|
|
407
|
-
- Escape brackets trong CSS: `.text-\[18px\]` (không phải `.text-[18px]`)
|
|
408
|
-
- Có thể cần `!important` để override Tailwind classes
|
|
409
|
-
- Dùng `[&_selector]` trong Tailwind để target nested elements
|
|
410
|
-
|
|
411
|
-
### Example 6: Access từ HTML (ESM)
|
|
412
|
-
|
|
413
|
-
```html
|
|
414
|
-
<script type="module">
|
|
415
|
-
import { ShowroomVisualizer } from 'http://localhost:3000/dist/index.js';
|
|
416
|
-
|
|
417
|
-
const CustomMarker = (props) => {
|
|
418
|
-
const React = window.React;
|
|
419
|
-
const { marker, active, size, onClick } = props;
|
|
420
|
-
|
|
421
|
-
return React.createElement(
|
|
422
|
-
'div',
|
|
423
|
-
{
|
|
424
|
-
style: {
|
|
425
|
-
width: size + 'px',
|
|
426
|
-
height: size + 'px',
|
|
427
|
-
background: active ? 'red' : 'blue',
|
|
428
|
-
borderRadius: '50%',
|
|
429
|
-
cursor: 'pointer',
|
|
430
|
-
},
|
|
431
|
-
onClick: () => onClick(marker),
|
|
432
|
-
},
|
|
433
|
-
active ? '📍' : '📌'
|
|
434
|
-
);
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
ShowroomVisualizer({
|
|
438
|
-
elementId: 'tour-container',
|
|
439
|
-
customLayout: {
|
|
440
|
-
Floorplan: {
|
|
441
|
-
Minimap: {
|
|
442
|
-
Marker: CustomMarker,
|
|
443
|
-
},
|
|
444
|
-
},
|
|
445
|
-
},
|
|
446
|
-
});
|
|
447
|
-
</script>
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
> ⚠️ **Không hỗ trợ trên Web Component (`dist/web.js`)** vì custom layout yêu cầu cùng React instance.
|
|
451
|
-
|
|
452
|
-
### Custom Layout Best Practices
|
|
453
|
-
|
|
454
|
-
1. **Start Small** - Override đúng component cần thiết
|
|
455
|
-
2. **Reuse Defaults** - Import và reuse default components khi có thể
|
|
456
|
-
3. **Type Safety** - Sử dụng `CustomLayoutConfig` để tránh sai sót
|
|
457
|
-
4. **Test Thoroughly** - Test custom components với nhiều scenarios
|
|
458
|
-
5. **Document Changes** - Comment rõ những gì đã customize
|
|
459
|
-
|
|
460
|
-
### Available Components for Custom Layout
|
|
461
|
-
|
|
462
|
-
```tsx
|
|
463
|
-
import { CUSTOM_LAYOUT_COMPONENTS } from '@clikvn/showroom-visualizer';
|
|
464
|
-
|
|
465
|
-
console.log(CUSTOM_LAYOUT_COMPONENTS);
|
|
466
|
-
// {
|
|
467
|
-
// Floorplan: {
|
|
468
|
-
// path: 'src/components/SkinLayer/Floorplan/index.tsx',
|
|
469
|
-
// description: 'Main Floorplan container with bottom sheet',
|
|
470
|
-
// children: {
|
|
471
|
-
// Map: {
|
|
472
|
-
// path: 'src/components/SkinLayer/Floorplan/Map.tsx',
|
|
473
|
-
// description: 'Floorplan map wrapper with controls',
|
|
474
|
-
// },
|
|
475
|
-
// Minimap: {
|
|
476
|
-
// path: 'src/components/SkinLayer/Floorplan/Minimap/index.tsx',
|
|
477
|
-
// description: 'Interactive minimap with markers and polygons',
|
|
478
|
-
// children: {
|
|
479
|
-
// Marker: {
|
|
480
|
-
// path: 'src/components/SkinLayer/Floorplan/Minimap/MiniMapMarker.tsx',
|
|
481
|
-
// description: 'Individual marker on minimap',
|
|
482
|
-
// },
|
|
483
|
-
// Polygon: {
|
|
484
|
-
// path: 'src/components/SkinLayer/Floorplan/Minimap/MiniMapPolygons/index.tsx',
|
|
485
|
-
// description: 'Polygon areas on minimap',
|
|
486
|
-
// },
|
|
487
|
-
// Radar: {
|
|
488
|
-
// path: 'src/components/SkinLayer/Floorplan/Minimap/index.tsx',
|
|
489
|
-
// description: 'Radar element inside minimap',
|
|
490
|
-
// },
|
|
491
|
-
// },
|
|
492
|
-
// },
|
|
493
|
-
// },
|
|
494
|
-
// },
|
|
495
|
-
// }
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
### Custom Layout Path Structure
|
|
499
|
-
|
|
500
|
-
Cấu trúc path cho custom layout:
|
|
501
|
-
|
|
502
|
-
```
|
|
503
|
-
Floorplan
|
|
504
|
-
├── Map → ['Floorplan', 'Map']
|
|
505
|
-
└── Minimap → ['Floorplan', 'Minimap']
|
|
506
|
-
├── Marker → ['Floorplan', 'Minimap', 'Marker']
|
|
507
|
-
├── Polygon → ['Floorplan', 'Minimap', 'Polygon']
|
|
508
|
-
└── Radar → ['Floorplan', 'Minimap', 'Radar']
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
**Ví dụ override nhiều components:**
|
|
512
|
-
|
|
513
|
-
```tsx
|
|
514
|
-
<ShowroomVisualizer
|
|
515
|
-
customLayout={{
|
|
516
|
-
Floorplan: {
|
|
517
|
-
// Override Map component
|
|
518
|
-
Map: MyCustomMap,
|
|
519
|
-
Minimap: {
|
|
520
|
-
// Override Minimap component
|
|
521
|
-
default: MyCustomMinimap,
|
|
522
|
-
// Override Marker
|
|
523
|
-
Marker: MyCustomMarker,
|
|
524
|
-
// Override Polygon
|
|
525
|
-
Polygon: MyCustomPolygon,
|
|
526
|
-
// Radar giữ nguyên default
|
|
527
|
-
},
|
|
528
|
-
},
|
|
529
|
-
}}
|
|
530
|
-
/>
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
---
|
|
534
|
-
|
|
535
|
-
## Headless Hooks Examples
|
|
536
|
-
|
|
537
|
-
### Example 7: External Controls (UI ở ngoài)
|
|
538
|
-
|
|
539
|
-
```tsx
|
|
540
|
-
import { useState } from 'react';
|
|
541
|
-
import {
|
|
542
|
-
ShowroomVisualizer,
|
|
543
|
-
useShowroomControls
|
|
544
|
-
} from 'showroom-visualizer';
|
|
545
|
-
|
|
546
|
-
function ExternalDashboard() {
|
|
547
|
-
const controls = useShowroomControls();
|
|
548
|
-
|
|
549
|
-
if (!controls.tourReady) {
|
|
550
|
-
return <div>Loading tour...</div>;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return (
|
|
554
|
-
<div className="dashboard" style={{ padding: '20px', background: '#f0f0f0' }}>
|
|
555
|
-
<h1>Tour Dashboard</h1>
|
|
556
|
-
|
|
557
|
-
{/* Tour Info */}
|
|
558
|
-
<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>
|
|
562
|
-
</div>
|
|
563
|
-
|
|
564
|
-
{/* Quick Actions */}
|
|
565
|
-
<div className="actions" style={{ marginTop: '20px' }}>
|
|
566
|
-
<button onClick={controls.toggleSound}>
|
|
567
|
-
{controls.viewport.tourSoundPlaying ? 'Mute' : 'Unmute'}
|
|
568
|
-
</button>
|
|
569
|
-
<button onClick={controls.toggleFullscreen}>
|
|
570
|
-
Fullscreen
|
|
571
|
-
</button>
|
|
572
|
-
<button onClick={controls.viewport.takeScreenshot}>
|
|
573
|
-
Screenshot
|
|
574
|
-
</button>
|
|
575
|
-
</div>
|
|
576
|
-
|
|
577
|
-
{/* Scene Selector */}
|
|
578
|
-
<div style={{ marginTop: '20px' }}>
|
|
579
|
-
<label>Jump to Scene: </label>
|
|
580
|
-
<select onChange={(e) => controls.goToScene(e.target.value)}>
|
|
581
|
-
{controls.navigation.scenes.map(scene => (
|
|
582
|
-
<option key={scene.id} value={scene.id}>
|
|
583
|
-
{scene.name}
|
|
584
|
-
</option>
|
|
585
|
-
))}
|
|
586
|
-
</select>
|
|
587
|
-
</div>
|
|
588
|
-
</div>
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
export default function App() {
|
|
593
|
-
return (
|
|
594
|
-
<div style={{ display: 'grid', gridTemplateColumns: '300px 1fr' }}>
|
|
595
|
-
{/* External Dashboard */}
|
|
596
|
-
<ExternalDashboard />
|
|
597
|
-
|
|
598
|
-
{/* Tour without UI */}
|
|
599
|
-
<ShowroomVisualizer
|
|
600
|
-
config={{ tourCode: 'my-tour' }}
|
|
601
|
-
disableDefaultUI={true}
|
|
602
|
-
/>
|
|
603
|
-
</div>
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
### Example 6: Auto-play Scenario với Progress
|
|
609
|
-
|
|
610
|
-
```tsx
|
|
611
|
-
import { ShowroomVisualizer, useShowroomControls } from 'showroom-visualizer';
|
|
612
|
-
|
|
613
|
-
function AutoPlayController() {
|
|
614
|
-
const scenario = useScenarioControl();
|
|
615
|
-
|
|
616
|
-
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
|
-
{!scenario.isPlaying ? (
|
|
631
|
-
<button
|
|
632
|
-
onClick={() => scenario.playScenario('intro')}
|
|
633
|
-
style={{
|
|
634
|
-
background: '#4CAF50',
|
|
635
|
-
color: 'white',
|
|
636
|
-
border: 'none',
|
|
637
|
-
padding: '10px 20px',
|
|
638
|
-
borderRadius: '20px',
|
|
639
|
-
cursor: 'pointer',
|
|
640
|
-
}}
|
|
641
|
-
>
|
|
642
|
-
▶ Play Guided Tour
|
|
643
|
-
</button>
|
|
644
|
-
) : (
|
|
645
|
-
<>
|
|
646
|
-
<button onClick={scenario.pauseScenario}>⏸</button>
|
|
647
|
-
<button onClick={scenario.stopScenario}>⏹</button>
|
|
648
|
-
<div>
|
|
649
|
-
Step {scenario.scenarioCurrentStep?.step || 0}
|
|
650
|
-
{scenario.activeScenario && ` of ${scenario.activeScenario.actions.length}`}
|
|
651
|
-
</div>
|
|
652
|
-
</>
|
|
653
|
-
)}
|
|
654
|
-
</div>
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
### Example 7: POI Explorer
|
|
660
|
-
|
|
661
|
-
```tsx
|
|
662
|
-
import { ShowroomVisualizer, usePOIInteraction, useTourCore } from 'showroom-visualizer';
|
|
663
|
-
|
|
664
|
-
function POIExplorer() {
|
|
665
|
-
const poi = usePOIInteraction();
|
|
666
|
-
const tour = useTourCore();
|
|
667
|
-
|
|
668
|
-
// Get all POIs from current scene
|
|
669
|
-
const currentScenePOIs = tour.activeScene?.pois || [];
|
|
670
|
-
|
|
671
|
-
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
|
-
}}>
|
|
683
|
-
<h3>Points of Interest</h3>
|
|
684
|
-
|
|
685
|
-
<button onClick={poi.toggleLabels} style={{ marginBottom: '10px' }}>
|
|
686
|
-
{poi.labelVisible ? 'Hide' : 'Show'} All Labels
|
|
687
|
-
</button>
|
|
688
|
-
|
|
689
|
-
<ul style={{ listStyle: 'none', padding: 0 }}>
|
|
690
|
-
{currentScenePOIs.map(poiItem => (
|
|
691
|
-
<li
|
|
692
|
-
key={poiItem.id}
|
|
693
|
-
onClick={() => poi.openPoiDetail(poiItem.code)}
|
|
694
|
-
style={{
|
|
695
|
-
padding: '10px',
|
|
696
|
-
margin: '5px 0',
|
|
697
|
-
background: poi.activePoiCode === poiItem.code ? '#e3f2fd' : '#f5f5f5',
|
|
698
|
-
borderRadius: '5px',
|
|
699
|
-
cursor: 'pointer',
|
|
700
|
-
border: poi.activePoiCode === poiItem.code ? '2px solid #2196F3' : 'none'
|
|
701
|
-
}}
|
|
702
|
-
>
|
|
703
|
-
<strong>{poiItem.name}</strong>
|
|
704
|
-
<p style={{ fontSize: '12px', margin: '5px 0 0 0', color: '#666' }}>
|
|
705
|
-
{poiItem.type}
|
|
706
|
-
</p>
|
|
707
|
-
</li>
|
|
708
|
-
))}
|
|
709
|
-
</ul>
|
|
710
|
-
</div>
|
|
711
|
-
);
|
|
712
|
-
}
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
---
|
|
716
|
-
|
|
717
|
-
## Advanced Examples
|
|
718
|
-
|
|
719
|
-
### Example 8: Complete Custom UI với Tất cả Features
|
|
720
|
-
|
|
721
|
-
```tsx
|
|
722
|
-
import {
|
|
723
|
-
ShowroomVisualizer,
|
|
724
|
-
useShowroomControls,
|
|
725
|
-
Floorplan,
|
|
726
|
-
PinActions,
|
|
727
|
-
} from 'showroom-visualizer';
|
|
728
|
-
|
|
729
|
-
function CompleteCustomUI() {
|
|
730
|
-
const controls = useShowroomControls();
|
|
731
|
-
|
|
732
|
-
return (
|
|
733
|
-
<>
|
|
734
|
-
{/* 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
|
-
}}>
|
|
748
|
-
<h1 style={{ margin: 0, fontSize: '18px' }}>
|
|
749
|
-
{controls.activeScene?.name}
|
|
750
|
-
</h1>
|
|
751
|
-
|
|
752
|
-
<div style={{ marginLeft: 'auto', display: 'flex', gap: '10px' }}>
|
|
753
|
-
<button onClick={controls.toggleSound}>
|
|
754
|
-
{controls.viewport.tourSoundPlaying ? '🔊' : '🔇'}
|
|
755
|
-
</button>
|
|
756
|
-
<button onClick={controls.toggleFullscreen}>
|
|
757
|
-
⛶
|
|
758
|
-
</button>
|
|
759
|
-
</div>
|
|
760
|
-
</div>
|
|
761
|
-
|
|
762
|
-
{/* 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
|
-
}}>
|
|
777
|
-
<button
|
|
778
|
-
onClick={controls.goToPreviousScene}
|
|
779
|
-
disabled={!controls.navigation.hasPreviousScene}
|
|
780
|
-
>
|
|
781
|
-
← Previous
|
|
782
|
-
</button>
|
|
783
|
-
|
|
784
|
-
<div style={{ flex: 1, textAlign: 'center' }}>
|
|
785
|
-
Scene {controls.navigation.scenes.findIndex(s => s.id === controls.activeScene?.id) + 1}
|
|
786
|
-
{' '} of {controls.navigation.totalScenes}
|
|
787
|
-
</div>
|
|
788
|
-
|
|
789
|
-
<button
|
|
790
|
-
onClick={controls.goToNextScene}
|
|
791
|
-
disabled={!controls.navigation.hasNextScene}
|
|
792
|
-
>
|
|
793
|
-
Next →
|
|
794
|
-
</button>
|
|
795
|
-
</div>
|
|
796
|
-
|
|
797
|
-
{/* Right Sidebar - Pin Actions */}
|
|
798
|
-
<div style={{
|
|
799
|
-
position: 'absolute',
|
|
800
|
-
right: 20,
|
|
801
|
-
top: '50%',
|
|
802
|
-
transform: 'translateY(-50%)',
|
|
803
|
-
zIndex: 1000
|
|
804
|
-
}}>
|
|
805
|
-
<PinActions />
|
|
806
|
-
</div>
|
|
807
|
-
|
|
808
|
-
{/* Minimap */}
|
|
809
|
-
{controls.showFloorplan && (
|
|
810
|
-
<div style={{
|
|
811
|
-
position: 'absolute',
|
|
812
|
-
left: 20,
|
|
813
|
-
bottom: 100,
|
|
814
|
-
width: '250px',
|
|
815
|
-
zIndex: 1000
|
|
816
|
-
}}>
|
|
817
|
-
<Floorplan />
|
|
818
|
-
</div>
|
|
819
|
-
)}
|
|
820
|
-
</>
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
export default function App() {
|
|
825
|
-
return (
|
|
826
|
-
<ShowroomVisualizer
|
|
827
|
-
config={{ tourCode: 'my-tour' }}
|
|
828
|
-
disableDefaultUI={true}
|
|
829
|
-
>
|
|
830
|
-
<CompleteCustomUI />
|
|
831
|
-
</ShowroomVisualizer>
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
> **Lưu ý:** `customLayout` prop chỉ dùng cho **component override** (CustomLayoutConfig). Để tạo custom UI hoàn toàn, dùng `disableDefaultUI={true}` và render children components.
|
|
837
|
-
|
|
838
|
-
### Example 9: Mobile-first Custom UI
|
|
839
|
-
|
|
840
|
-
```tsx
|
|
841
|
-
import { ShowroomVisualizer, useShowroomControls } from 'showroom-visualizer';
|
|
842
|
-
|
|
843
|
-
function MobileUI() {
|
|
844
|
-
const controls = useShowroomControls();
|
|
845
|
-
const [menuOpen, setMenuOpen] = useState(false);
|
|
846
|
-
|
|
847
|
-
return (
|
|
848
|
-
<>
|
|
849
|
-
{/* Hamburger Menu Button */}
|
|
850
|
-
<button
|
|
851
|
-
onClick={() => setMenuOpen(!menuOpen)}
|
|
852
|
-
style={{
|
|
853
|
-
position: 'absolute',
|
|
854
|
-
top: 10,
|
|
855
|
-
right: 10,
|
|
856
|
-
zIndex: 2000,
|
|
857
|
-
padding: '10px',
|
|
858
|
-
background: 'rgba(0,0,0,0.5)',
|
|
859
|
-
color: 'white',
|
|
860
|
-
border: 'none',
|
|
861
|
-
borderRadius: '5px',
|
|
862
|
-
}}
|
|
863
|
-
>
|
|
864
|
-
☰
|
|
865
|
-
</button>
|
|
866
|
-
|
|
867
|
-
{/* Slide-out Menu */}
|
|
868
|
-
{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
|
-
}}>
|
|
880
|
-
<button onClick={() => setMenuOpen(false)}>✕ Close</button>
|
|
881
|
-
|
|
882
|
-
<h2>Scenes</h2>
|
|
883
|
-
{controls.navigation.scenes.map(scene => (
|
|
884
|
-
<button
|
|
885
|
-
key={scene.id}
|
|
886
|
-
onClick={() => {
|
|
887
|
-
controls.goToScene(scene.id);
|
|
888
|
-
setMenuOpen(false);
|
|
889
|
-
}}
|
|
890
|
-
style={{
|
|
891
|
-
display: 'block',
|
|
892
|
-
width: '100%',
|
|
893
|
-
padding: '15px',
|
|
894
|
-
marginBottom: '10px',
|
|
895
|
-
background: scene.id === controls.activeScene?.id ? '#007bff' : '#f5f5f5',
|
|
896
|
-
color: scene.id === controls.activeScene?.id ? 'white' : 'black',
|
|
897
|
-
border: 'none',
|
|
898
|
-
borderRadius: '5px',
|
|
899
|
-
textAlign: 'left',
|
|
900
|
-
}}
|
|
901
|
-
>
|
|
902
|
-
{scene.name}
|
|
903
|
-
</button>
|
|
904
|
-
))}
|
|
905
|
-
</div>
|
|
906
|
-
)}
|
|
907
|
-
|
|
908
|
-
{/* 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
|
-
}}>
|
|
923
|
-
<button
|
|
924
|
-
onClick={controls.goToPreviousScene}
|
|
925
|
-
disabled={!controls.navigation.hasPreviousScene}
|
|
926
|
-
style={{
|
|
927
|
-
padding: '10px 20px',
|
|
928
|
-
background: 'transparent',
|
|
929
|
-
color: 'white',
|
|
930
|
-
border: '1px solid white',
|
|
931
|
-
borderRadius: '20px',
|
|
932
|
-
}}
|
|
933
|
-
>
|
|
934
|
-
←
|
|
935
|
-
</button>
|
|
936
|
-
|
|
937
|
-
<span>{controls.activeScene?.name}</span>
|
|
938
|
-
|
|
939
|
-
<button
|
|
940
|
-
onClick={controls.goToNextScene}
|
|
941
|
-
disabled={!controls.navigation.hasNextScene}
|
|
942
|
-
style={{
|
|
943
|
-
padding: '10px 20px',
|
|
944
|
-
background: 'transparent',
|
|
945
|
-
color: 'white',
|
|
946
|
-
border: '1px solid white',
|
|
947
|
-
borderRadius: '20px',
|
|
948
|
-
}}
|
|
949
|
-
>
|
|
950
|
-
→
|
|
951
|
-
</button>
|
|
952
|
-
</div>
|
|
953
|
-
</>
|
|
954
|
-
);
|
|
955
|
-
}
|
|
956
|
-
```
|
|
957
|
-
|
|
958
|
-
---
|
|
959
|
-
|
|
960
|
-
## 🎓 Best Practices
|
|
961
|
-
|
|
962
|
-
1. **Performance**: Sử dụng `useMemo` và `useCallback` khi cần
|
|
963
|
-
2. **Type Safety**: Import types từ `'showroom-visualizer'`
|
|
964
|
-
3. **Error Handling**: Check `tourReady` trước khi render UI
|
|
965
|
-
4. **Responsive**: Test trên nhiều screen sizes
|
|
966
|
-
5. **Accessibility**: Thêm ARIA labels cho custom buttons
|
|
967
|
-
|