@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limrun/ui",
3
- "version": "0.4.0-rc.9",
3
+ "version": "0.4.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,11 +1,24 @@
1
1
  .rc-container {
2
2
  position: relative;
3
- display: inline-block;
4
- height: 100%;
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
- height: 100%;
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: 100%;
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
- width: auto;
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/iphone16pro_black.webp';
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/iphone16pro_black_landscape.webp';
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
- const deviceConfig = {
109
+ // Video position percentages are relative to the frame image dimensions
110
+ const deviceConfig: Record<DevicePlatform, DeviceConfig> = {
96
111
  ios: {
97
- frameImage: iphoneFrameImage,
98
- frameImageLandscape: iphoneFrameImageLandscape,
99
- frameWidthMultiplier: 1.0841,
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
- frameImage: pixelFrameImage,
106
- frameImageLandscape: pixelFrameImageLandscape,
107
- frameWidthMultiplier: 1.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
- // Resize phone frame and video border-radius relative to video size
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
- if (!video || !frame || !showFrame) return;
795
-
796
- const resizeObserver = new ResizeObserver((entries) => {
797
- for (const entry of entries) {
798
- const landscape = entry.contentRect.width > entry.contentRect.height;
799
- setIsLandscape(landscape);
800
- const videoWidth = entry.contentRect.width;
801
- const videoHeight = entry.contentRect.height;
802
- const originalFrameWidth = frame.clientWidth;
803
- const newFrameWidth = videoWidth * config.frameWidthMultiplier;
804
- // The video is too small up until the first frame is visible, so we need to make sure we don't scale the frame down too much.
805
- if (!landscape && newFrameWidth > 0.9*originalFrameWidth) {
806
- // This is mostly for iOS where the video size is slightly inconsistent.
807
- frame.style.width = `${videoWidth * config.frameWidthMultiplier}px`;
808
- }
809
- video.style.borderRadius = `${(entry.contentRect.width > entry.contentRect.height ? videoHeight : videoWidth) * config.videoBorderRadiusMultiplier}px`;
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', // Use custom CSS class instead of Tailwind
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.frameImageLandscape : config.frameImage}
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 ? (platform === 'ios' ? (isLandscape ? 'rc-video-ios-landscape' : 'rc-video-ios-portrait') : isLandscape ? 'rc-video-android-landscape' : 'rc-video-android-portrait') : 'rc-video-frameless',
1025
+ !showFrame && 'rc-video-frameless',
960
1026
  !videoLoaded && 'rc-video-loading',
961
1027
  )}
962
- style={
963
- config.loadingLogo
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>