@limrun/ui 0.4.0-rc.9 → 0.4.1
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.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +354 -331
- package/package.json +1 -1
- package/src/assets/iphone16pro_black_bg.webp +0 -0
- package/src/assets/iphone16pro_black_landscape_bg.webp +0 -0
- package/src/components/remote-control.css +41 -29
- package/src/components/remote-control.tsx +100 -33
- package/dist/demo.d.ts +0 -1
- package/index.html +0 -181
- package/src/assets/CHf0W2V8oUYM0sz1zN3AItWTl78EDajyAcUfQPYA5LRzxR69YBM334mnZAMo2PCKcIo=w480-h960-rw +0 -0
- package/src/demo.tsx +0 -192
- package/src/image.png +0 -0
- package/src/landscape.png +0 -0
- package/src/portrait.png +0 -0
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
.rc-container {
|
|
2
2
|
position: relative;
|
|
3
|
-
display:
|
|
4
|
-
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
5
6
|
box-sizing: border-box;
|
|
6
7
|
isolation: isolate;
|
|
7
8
|
|
|
8
9
|
touch-action: none;
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.rc-container-portrait {
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.rc-container-landscape {
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
9
22
|
}
|
|
10
23
|
|
|
11
24
|
.rc-phone-frame {
|
|
@@ -14,23 +27,45 @@
|
|
|
14
27
|
display: block;
|
|
15
28
|
pointer-events: none;
|
|
16
29
|
user-select: none;
|
|
30
|
+
width: auto;
|
|
31
|
+
height: auto;
|
|
32
|
+
max-width: 100%;
|
|
33
|
+
max-height: 100%;
|
|
17
34
|
}
|
|
18
35
|
|
|
19
36
|
.rc-phone-frame-portrait {
|
|
20
|
-
|
|
37
|
+
width: auto;
|
|
38
|
+
height: auto;
|
|
39
|
+
max-width: 100%;
|
|
40
|
+
max-height: 100%;
|
|
21
41
|
}
|
|
22
42
|
|
|
23
43
|
.rc-phone-frame-landscape {
|
|
24
|
-
width:
|
|
44
|
+
width: auto;
|
|
45
|
+
height: auto;
|
|
46
|
+
max-width: 100%;
|
|
47
|
+
max-height: 100%;
|
|
25
48
|
}
|
|
26
49
|
|
|
27
50
|
.rc-video {
|
|
28
51
|
position: absolute;
|
|
29
|
-
|
|
52
|
+
/* Size is controlled by inline style; defaults are for frameless mode */
|
|
53
|
+
width: 100%;
|
|
54
|
+
height: 100%;
|
|
55
|
+
z-index: 30;
|
|
30
56
|
outline: none;
|
|
31
57
|
pointer-events: none;
|
|
32
58
|
cursor: none;
|
|
33
59
|
background-color: black;
|
|
60
|
+
object-fit: contain;
|
|
61
|
+
object-position: center center;
|
|
62
|
+
inset: 0;
|
|
63
|
+
|
|
64
|
+
top: 50%;
|
|
65
|
+
left: 50%;
|
|
66
|
+
transform: translate(-50%, -50%);
|
|
67
|
+
right: auto;
|
|
68
|
+
bottom: auto;
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
.rc-video-frameless {
|
|
@@ -38,32 +73,9 @@
|
|
|
38
73
|
height: 100%;
|
|
39
74
|
top: auto;
|
|
40
75
|
left: auto;
|
|
76
|
+
width: 100%;
|
|
41
77
|
}
|
|
42
78
|
|
|
43
79
|
.rc-video-loading {
|
|
44
80
|
aspect-ratio: 9 / 19.5;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.rc-video-ios-portrait {
|
|
48
|
-
top: 1.61%;
|
|
49
|
-
left: 3.9%;
|
|
50
|
-
height: 96.78%;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.rc-video-ios-landscape {
|
|
54
|
-
top: 3.9%;
|
|
55
|
-
left: 1.61%;
|
|
56
|
-
width: 96.78%;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.rc-video-android-portrait {
|
|
60
|
-
top: 2.25%;
|
|
61
|
-
left: 4.5%;
|
|
62
|
-
height: 95.9%;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.rc-video-android-landscape {
|
|
66
|
-
left: 2.25%;
|
|
67
|
-
top: 4.5%;
|
|
68
|
-
width: 95.9%;
|
|
69
81
|
}
|
|
@@ -4,10 +4,10 @@ import './remote-control.css';
|
|
|
4
4
|
|
|
5
5
|
import { ANDROID_KEYS, AMOTION_EVENT, codeMap } from '../core/constants';
|
|
6
6
|
|
|
7
|
-
import iphoneFrameImage from '../assets/
|
|
7
|
+
import iphoneFrameImage from '../assets/iphone16pro_black_bg.webp';
|
|
8
8
|
import pixelFrameImage from '../assets/pixel9_black.webp';
|
|
9
9
|
import pixelFrameImageLandscape from '../assets/pixel9_black_landscape.webp';
|
|
10
|
-
import iphoneFrameImageLandscape from '../assets/
|
|
10
|
+
import iphoneFrameImageLandscape from '../assets/iphone16pro_black_landscape_bg.webp';
|
|
11
11
|
import appleLogoSvg from '../assets/Apple_logo_white.svg';
|
|
12
12
|
import androidBootImage from '../assets/android_boot.webp';
|
|
13
13
|
import {
|
|
@@ -91,23 +91,50 @@ const detectPlatform = (url: string): DevicePlatform => {
|
|
|
91
91
|
return 'ios';
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
+
type DeviceConfig = {
|
|
95
|
+
videoBorderRadiusMultiplier: number;
|
|
96
|
+
loadingLogo: string;
|
|
97
|
+
loadingLogoSize: string;
|
|
98
|
+
videoPosition: {
|
|
99
|
+
portrait: { heightMultiplier?: number; widthMultiplier?: number; };
|
|
100
|
+
landscape: { heightMultiplier?: number; widthMultiplier?: number; };
|
|
101
|
+
};
|
|
102
|
+
frame: {
|
|
103
|
+
image: string;
|
|
104
|
+
imageLandscape: string;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
94
108
|
// Device-specific configuration for frame sizing and video positioning
|
|
95
|
-
|
|
109
|
+
// Video position percentages are relative to the frame image dimensions
|
|
110
|
+
const deviceConfig: Record<DevicePlatform, DeviceConfig> = {
|
|
96
111
|
ios: {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
frame: {
|
|
113
|
+
image: iphoneFrameImage,
|
|
114
|
+
imageLandscape: iphoneFrameImageLandscape,
|
|
115
|
+
},
|
|
100
116
|
videoBorderRadiusMultiplier: 0.15,
|
|
101
117
|
loadingLogo: appleLogoSvg,
|
|
102
118
|
loadingLogoSize: '20%',
|
|
119
|
+
// Video position as percentage of frame dimensions
|
|
120
|
+
videoPosition: {
|
|
121
|
+
portrait: { heightMultiplier: 0.9678 },
|
|
122
|
+
landscape: { widthMultiplier: 0.9678 },
|
|
123
|
+
},
|
|
103
124
|
},
|
|
104
125
|
android: {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
126
|
+
frame: {
|
|
127
|
+
image: pixelFrameImage,
|
|
128
|
+
imageLandscape: pixelFrameImageLandscape,
|
|
129
|
+
},
|
|
108
130
|
videoBorderRadiusMultiplier: 0.13,
|
|
109
131
|
loadingLogo: androidBootImage,
|
|
110
132
|
loadingLogoSize: '40%',
|
|
133
|
+
// Video position as percentage of frame dimensions
|
|
134
|
+
videoPosition: {
|
|
135
|
+
portrait: { heightMultiplier: 0.967 },
|
|
136
|
+
landscape: { widthMultiplier: 0.962 },
|
|
137
|
+
},
|
|
111
138
|
},
|
|
112
139
|
};
|
|
113
140
|
|
|
@@ -147,6 +174,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
147
174
|
const frameRef = useRef<HTMLImageElement>(null);
|
|
148
175
|
const [videoLoaded, setVideoLoaded] = useState(false);
|
|
149
176
|
const [isLandscape, setIsLandscape] = useState(false);
|
|
177
|
+
const [videoStyle, setVideoStyle] = useState<React.CSSProperties>({});
|
|
150
178
|
const wsRef = useRef<WebSocket | null>(null);
|
|
151
179
|
const peerConnectionRef = useRef<RTCPeerConnection | null>(null);
|
|
152
180
|
const dataChannelRef = useRef<RTCDataChannel | null>(null);
|
|
@@ -787,33 +815,70 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
787
815
|
};
|
|
788
816
|
}, [url, token, propSessionId]);
|
|
789
817
|
|
|
790
|
-
//
|
|
818
|
+
// Calculate video position and border-radius based on frame dimensions
|
|
791
819
|
useEffect(() => {
|
|
792
820
|
const video = videoRef.current;
|
|
793
821
|
const frame = frameRef.current;
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
822
|
+
|
|
823
|
+
if (!video) return;
|
|
824
|
+
|
|
825
|
+
// If no frame, no positioning needed
|
|
826
|
+
if (!showFrame || !frame) {
|
|
827
|
+
setVideoStyle({});
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const updateVideoPosition = () => {
|
|
832
|
+
const frameWidth = frame.clientWidth;
|
|
833
|
+
const frameHeight = frame.clientHeight;
|
|
834
|
+
|
|
835
|
+
if (frameWidth === 0 || frameHeight === 0) return;
|
|
836
|
+
|
|
837
|
+
// Determine landscape based on video's intrinsic dimensions
|
|
838
|
+
const landscape = video.videoWidth > video.videoHeight;
|
|
839
|
+
setIsLandscape(landscape);
|
|
840
|
+
|
|
841
|
+
const pos = landscape ? config.videoPosition.landscape : config.videoPosition.portrait;
|
|
842
|
+
let newStyle: React.CSSProperties = {};
|
|
843
|
+
if (pos.heightMultiplier) {
|
|
844
|
+
newStyle.height = `${frameHeight * pos.heightMultiplier}px`;
|
|
845
|
+
// Let the other dimension follow the video stream's intrinsic aspect ratio.
|
|
846
|
+
newStyle.width = 'auto';
|
|
847
|
+
} else if (pos.widthMultiplier) {
|
|
848
|
+
newStyle.width = `${frameWidth * pos.widthMultiplier}px`;
|
|
849
|
+
// Let the other dimension follow the video stream's intrinsic aspect ratio.
|
|
850
|
+
newStyle.height = 'auto';
|
|
810
851
|
}
|
|
852
|
+
newStyle.borderRadius = `${landscape ? frameHeight * config.videoBorderRadiusMultiplier : frameWidth * config.videoBorderRadiusMultiplier}px`;
|
|
853
|
+
setVideoStyle(newStyle);
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
857
|
+
updateVideoPosition();
|
|
811
858
|
});
|
|
812
859
|
|
|
860
|
+
resizeObserver.observe(frame);
|
|
813
861
|
resizeObserver.observe(video);
|
|
862
|
+
|
|
863
|
+
// Also update when the frame image loads
|
|
864
|
+
frame.addEventListener('load', updateVideoPosition);
|
|
865
|
+
|
|
866
|
+
// Update when video metadata loads (to get correct intrinsic dimensions)
|
|
867
|
+
video.addEventListener('loadedmetadata', updateVideoPosition);
|
|
868
|
+
|
|
869
|
+
// IMPORTANT: When the WebRTC stream changes orientation, the intrinsic video size
|
|
870
|
+
// (videoWidth/videoHeight) can change without re-firing 'loadedmetadata'.
|
|
871
|
+
// The <video> element emits 'resize' in that case.
|
|
872
|
+
video.addEventListener('resize', updateVideoPosition);
|
|
873
|
+
|
|
874
|
+
// Initial calculation
|
|
875
|
+
updateVideoPosition();
|
|
814
876
|
|
|
815
877
|
return () => {
|
|
816
878
|
resizeObserver.disconnect();
|
|
879
|
+
video.removeEventListener('loadedmetadata', updateVideoPosition);
|
|
880
|
+
video.removeEventListener('resize', updateVideoPosition);
|
|
881
|
+
frame.removeEventListener('load', updateVideoPosition);
|
|
817
882
|
};
|
|
818
883
|
}, [config, showFrame]);
|
|
819
884
|
|
|
@@ -928,7 +993,8 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
928
993
|
return (
|
|
929
994
|
<div
|
|
930
995
|
className={clsx(
|
|
931
|
-
'rc-container',
|
|
996
|
+
'rc-container',
|
|
997
|
+
isLandscape ? 'rc-container-landscape' : 'rc-container-portrait', // Use custom CSS class instead of Tailwind
|
|
932
998
|
className,
|
|
933
999
|
)}
|
|
934
1000
|
style={{ touchAction: 'none' }} // Keep touchAction none for the container
|
|
@@ -946,7 +1012,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
946
1012
|
{showFrame && (
|
|
947
1013
|
<img
|
|
948
1014
|
ref={frameRef}
|
|
949
|
-
src={isLandscape ? config.
|
|
1015
|
+
src={isLandscape ? config.frame.imageLandscape : config.frame.image}
|
|
950
1016
|
alt=""
|
|
951
1017
|
className={clsx('rc-phone-frame', isLandscape ? 'rc-phone-frame-landscape' : 'rc-phone-frame-portrait')}
|
|
952
1018
|
draggable={false}
|
|
@@ -956,19 +1022,20 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
956
1022
|
ref={videoRef}
|
|
957
1023
|
className={clsx(
|
|
958
1024
|
'rc-video',
|
|
959
|
-
showFrame
|
|
1025
|
+
!showFrame && 'rc-video-frameless',
|
|
960
1026
|
!videoLoaded && 'rc-video-loading',
|
|
961
1027
|
)}
|
|
962
|
-
style={
|
|
963
|
-
|
|
1028
|
+
style={{
|
|
1029
|
+
...videoStyle,
|
|
1030
|
+
...(config.loadingLogo
|
|
964
1031
|
? {
|
|
965
1032
|
backgroundImage: `url("${config.loadingLogo}")`,
|
|
966
1033
|
backgroundRepeat: 'no-repeat',
|
|
967
1034
|
backgroundPosition: 'center',
|
|
968
1035
|
backgroundSize: config.loadingLogoSize,
|
|
969
1036
|
}
|
|
970
|
-
: {}
|
|
971
|
-
}
|
|
1037
|
+
: {}),
|
|
1038
|
+
}}
|
|
972
1039
|
autoPlay
|
|
973
1040
|
playsInline
|
|
974
1041
|
muted
|
package/dist/demo.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/index.html
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>RemoteControl Demo - @limrun/ui</title>
|
|
7
|
-
<style>
|
|
8
|
-
* {
|
|
9
|
-
margin: 0;
|
|
10
|
-
padding: 0;
|
|
11
|
-
box-sizing: border-box;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
body {
|
|
15
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
16
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
-
min-height: 100vh;
|
|
18
|
-
padding: 20px;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
#root {
|
|
22
|
-
display: flex;
|
|
23
|
-
flex-direction: column;
|
|
24
|
-
gap: 20px;
|
|
25
|
-
max-width: 1400px;
|
|
26
|
-
margin: 0 auto;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.header {
|
|
30
|
-
text-align: center;
|
|
31
|
-
color: white;
|
|
32
|
-
padding: 20px;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.header h1 {
|
|
36
|
-
font-size: 2.5rem;
|
|
37
|
-
margin-bottom: 10px;
|
|
38
|
-
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.header p {
|
|
42
|
-
font-size: 1.1rem;
|
|
43
|
-
opacity: 0.9;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.demo-container {
|
|
47
|
-
background: white;
|
|
48
|
-
border-radius: 20px;
|
|
49
|
-
padding: 30px;
|
|
50
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.controls {
|
|
54
|
-
display: flex;
|
|
55
|
-
flex-direction: column;
|
|
56
|
-
gap: 15px;
|
|
57
|
-
margin-bottom: 30px;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.control-group {
|
|
61
|
-
display: flex;
|
|
62
|
-
flex-direction: column;
|
|
63
|
-
gap: 8px;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.control-group label {
|
|
67
|
-
font-weight: 600;
|
|
68
|
-
font-size: 0.9rem;
|
|
69
|
-
color: #374151;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.control-group input,
|
|
73
|
-
.control-group select {
|
|
74
|
-
padding: 10px 15px;
|
|
75
|
-
border: 2px solid #e5e7eb;
|
|
76
|
-
border-radius: 8px;
|
|
77
|
-
font-size: 0.95rem;
|
|
78
|
-
transition: border-color 0.2s;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.control-group input:focus,
|
|
82
|
-
.control-group select:focus {
|
|
83
|
-
outline: none;
|
|
84
|
-
border-color: #667eea;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
.button-group {
|
|
88
|
-
display: flex;
|
|
89
|
-
gap: 10px;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
button {
|
|
93
|
-
padding: 12px 24px;
|
|
94
|
-
border: none;
|
|
95
|
-
border-radius: 8px;
|
|
96
|
-
font-size: 1rem;
|
|
97
|
-
font-weight: 600;
|
|
98
|
-
cursor: pointer;
|
|
99
|
-
transition: all 0.2s;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
button.primary {
|
|
103
|
-
background: #667eea;
|
|
104
|
-
color: white;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
button.primary:hover {
|
|
108
|
-
background: #5568d3;
|
|
109
|
-
transform: translateY(-1px);
|
|
110
|
-
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
button.secondary {
|
|
114
|
-
background: #e5e7eb;
|
|
115
|
-
color: #374151;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
button.secondary:hover {
|
|
119
|
-
background: #d1d5db;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
button:disabled {
|
|
123
|
-
opacity: 0.5;
|
|
124
|
-
cursor: not-allowed;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.device-preview {
|
|
128
|
-
display: flex;
|
|
129
|
-
gap: 30px;
|
|
130
|
-
justify-content: center;
|
|
131
|
-
flex-wrap: wrap;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.preview-item {
|
|
135
|
-
flex: 1;
|
|
136
|
-
min-width: 300px;
|
|
137
|
-
max-width: 800px;
|
|
138
|
-
max-height: 800px;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.preview-item h3 {
|
|
142
|
-
text-align: center;
|
|
143
|
-
margin-bottom: 15px;
|
|
144
|
-
font-size: 1.2rem;
|
|
145
|
-
color: #374151;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.device-wrapper {
|
|
149
|
-
height: 100%;
|
|
150
|
-
width: 100%;
|
|
151
|
-
background: #f9fafb;
|
|
152
|
-
border-radius: 12px;
|
|
153
|
-
overflow: hidden;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.info-box {
|
|
157
|
-
background: #fef3c7;
|
|
158
|
-
border: 2px solid #fbbf24;
|
|
159
|
-
border-radius: 8px;
|
|
160
|
-
padding: 15px;
|
|
161
|
-
margin-bottom: 20px;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.info-box h4 {
|
|
165
|
-
color: #92400e;
|
|
166
|
-
margin-bottom: 8px;
|
|
167
|
-
font-size: 0.95rem;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.info-box p {
|
|
171
|
-
color: #78350f;
|
|
172
|
-
font-size: 0.9rem;
|
|
173
|
-
line-height: 1.5;
|
|
174
|
-
}
|
|
175
|
-
</style>
|
|
176
|
-
</head>
|
|
177
|
-
<body>
|
|
178
|
-
<div id="root"></div>
|
|
179
|
-
<script type="module" src="/src/demo.tsx"></script>
|
|
180
|
-
</body>
|
|
181
|
-
</html>
|