Eduards commited on
Commit
a0eb0a0
·
unverified ·
2 Parent(s): 1890c4e b6eef57

Merge pull request #550 from wonderwhy-er/pr-549

Browse files
Files changed (1) hide show
  1. app/components/workbench/Preview.tsx +234 -12
app/components/workbench/Preview.tsx CHANGED
@@ -4,11 +4,16 @@ import { IconButton } from '~/components/ui/IconButton';
4
  import { workbenchStore } from '~/lib/stores/workbench';
5
  import { PortDropdown } from './PortDropdown';
6
 
 
 
7
  export const Preview = memo(() => {
8
  const iframeRef = useRef<HTMLIFrameElement>(null);
 
9
  const inputRef = useRef<HTMLInputElement>(null);
 
10
  const [activePreviewIndex, setActivePreviewIndex] = useState(0);
11
  const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
 
12
  const hasSelectedPreview = useRef(false);
13
  const previews = useStore(workbenchStore.previews);
14
  const activePreview = previews[activePreviewIndex];
@@ -16,6 +21,23 @@ export const Preview = memo(() => {
16
  const [url, setUrl] = useState('');
17
  const [iframeUrl, setIframeUrl] = useState<string | undefined>();
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  useEffect(() => {
20
  if (!activePreview) {
21
  setUrl('');
@@ -25,10 +47,9 @@ export const Preview = memo(() => {
25
  }
26
 
27
  const { baseUrl } = activePreview;
28
-
29
  setUrl(baseUrl);
30
  setIframeUrl(baseUrl);
31
- }, [activePreview, iframeUrl]);
32
 
33
  const validateUrl = useCallback(
34
  (value: string) => {
@@ -56,14 +77,13 @@ export const Preview = memo(() => {
56
  [],
57
  );
58
 
59
- // when previews change, display the lowest port if user hasn't selected a preview
60
  useEffect(() => {
61
  if (previews.length > 1 && !hasSelectedPreview.current) {
62
  const minPortIndex = previews.reduce(findMinPortIndex, 0);
63
-
64
  setActivePreviewIndex(minPortIndex);
65
  }
66
- }, [previews]);
67
 
68
  const reloadPreview = () => {
69
  if (iframeRef.current) {
@@ -71,13 +91,134 @@ export const Preview = memo(() => {
71
  }
72
  };
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  return (
75
- <div className="w-full h-full flex flex-col">
76
  {isPortDropdownOpen && (
77
  <div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
78
  )}
79
  <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
80
  <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
 
81
  <div
82
  className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
83
  focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
@@ -101,6 +242,7 @@ export const Preview = memo(() => {
101
  }}
102
  />
103
  </div>
 
104
  {previews.length > 1 && (
105
  <PortDropdown
106
  activePreviewIndex={activePreviewIndex}
@@ -111,13 +253,93 @@ export const Preview = memo(() => {
111
  previews={previews}
112
  />
113
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  </div>
115
- <div className="flex-1 border-t border-bolt-elements-borderColor">
116
- {activePreview ? (
117
- <iframe ref={iframeRef} className="border-none w-full h-full bg-white" src={iframeUrl} />
118
- ) : (
119
- <div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
120
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  </div>
122
  </div>
123
  );
 
4
  import { workbenchStore } from '~/lib/stores/workbench';
5
  import { PortDropdown } from './PortDropdown';
6
 
7
+ type ResizeSide = 'left' | 'right' | null;
8
+
9
  export const Preview = memo(() => {
10
  const iframeRef = useRef<HTMLIFrameElement>(null);
11
+ const containerRef = useRef<HTMLDivElement>(null);
12
  const inputRef = useRef<HTMLInputElement>(null);
13
+
14
  const [activePreviewIndex, setActivePreviewIndex] = useState(0);
15
  const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
16
+ const [isFullscreen, setIsFullscreen] = useState(false);
17
  const hasSelectedPreview = useRef(false);
18
  const previews = useStore(workbenchStore.previews);
19
  const activePreview = previews[activePreviewIndex];
 
21
  const [url, setUrl] = useState('');
22
  const [iframeUrl, setIframeUrl] = useState<string | undefined>();
23
 
24
+ // Toggle between responsive mode and device mode
25
+ const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
26
+
27
+ // Use percentage for width
28
+ const [widthPercent, setWidthPercent] = useState<number>(37.5); // 375px assuming 1000px window width initially
29
+
30
+ const resizingState = useRef({
31
+ isResizing: false,
32
+ side: null as ResizeSide,
33
+ startX: 0,
34
+ startWidthPercent: 37.5,
35
+ windowWidth: window.innerWidth,
36
+ });
37
+
38
+ // Define the scaling factor
39
+ const SCALING_FACTOR = 2; // Adjust this value to increase/decrease sensitivity
40
+
41
  useEffect(() => {
42
  if (!activePreview) {
43
  setUrl('');
 
47
  }
48
 
49
  const { baseUrl } = activePreview;
 
50
  setUrl(baseUrl);
51
  setIframeUrl(baseUrl);
52
+ }, [activePreview]);
53
 
54
  const validateUrl = useCallback(
55
  (value: string) => {
 
77
  [],
78
  );
79
 
80
+ // When previews change, display the lowest port if user hasn't selected a preview
81
  useEffect(() => {
82
  if (previews.length > 1 && !hasSelectedPreview.current) {
83
  const minPortIndex = previews.reduce(findMinPortIndex, 0);
 
84
  setActivePreviewIndex(minPortIndex);
85
  }
86
+ }, [previews, findMinPortIndex]);
87
 
88
  const reloadPreview = () => {
89
  if (iframeRef.current) {
 
91
  }
92
  };
93
 
94
+ const toggleFullscreen = async () => {
95
+ if (!isFullscreen && containerRef.current) {
96
+ await containerRef.current.requestFullscreen();
97
+ } else if (document.fullscreenElement) {
98
+ await document.exitFullscreen();
99
+ }
100
+ };
101
+
102
+ useEffect(() => {
103
+ const handleFullscreenChange = () => {
104
+ setIsFullscreen(!!document.fullscreenElement);
105
+ };
106
+
107
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
108
+
109
+ return () => {
110
+ document.removeEventListener('fullscreenchange', handleFullscreenChange);
111
+ };
112
+ }, []);
113
+
114
+ const toggleDeviceMode = () => {
115
+ setIsDeviceModeOn((prev) => !prev);
116
+ };
117
+
118
+ const startResizing = (e: React.MouseEvent, side: ResizeSide) => {
119
+ if (!isDeviceModeOn) {
120
+ return;
121
+ }
122
+
123
+ // Prevent text selection
124
+ document.body.style.userSelect = 'none';
125
+
126
+ resizingState.current.isResizing = true;
127
+ resizingState.current.side = side;
128
+ resizingState.current.startX = e.clientX;
129
+ resizingState.current.startWidthPercent = widthPercent;
130
+ resizingState.current.windowWidth = window.innerWidth;
131
+
132
+ document.addEventListener('mousemove', onMouseMove);
133
+ document.addEventListener('mouseup', onMouseUp);
134
+
135
+ e.preventDefault(); // Prevent any text selection on mousedown
136
+ };
137
+
138
+ const onMouseMove = (e: MouseEvent) => {
139
+ if (!resizingState.current.isResizing) {
140
+ return;
141
+ }
142
+
143
+ const dx = e.clientX - resizingState.current.startX;
144
+ const windowWidth = resizingState.current.windowWidth;
145
+
146
+ // Apply scaling factor to increase sensitivity
147
+ const dxPercent = (dx / windowWidth) * 100 * SCALING_FACTOR;
148
+
149
+ let newWidthPercent = resizingState.current.startWidthPercent;
150
+
151
+ if (resizingState.current.side === 'right') {
152
+ newWidthPercent = resizingState.current.startWidthPercent + dxPercent;
153
+ } else if (resizingState.current.side === 'left') {
154
+ newWidthPercent = resizingState.current.startWidthPercent - dxPercent;
155
+ }
156
+
157
+ // Clamp the width between 10% and 90%
158
+ newWidthPercent = Math.max(10, Math.min(newWidthPercent, 90));
159
+
160
+ setWidthPercent(newWidthPercent);
161
+ };
162
+
163
+ const onMouseUp = () => {
164
+ resizingState.current.isResizing = false;
165
+ resizingState.current.side = null;
166
+ document.removeEventListener('mousemove', onMouseMove);
167
+ document.removeEventListener('mouseup', onMouseUp);
168
+
169
+ // Restore text selection
170
+ document.body.style.userSelect = '';
171
+ };
172
+
173
+ // Handle window resize to ensure widthPercent remains valid
174
+ useEffect(() => {
175
+ const handleWindowResize = () => {
176
+ /*
177
+ * Optional: Adjust widthPercent if necessary
178
+ * For now, since widthPercent is relative, no action is needed
179
+ */
180
+ };
181
+
182
+ window.addEventListener('resize', handleWindowResize);
183
+
184
+ return () => {
185
+ window.removeEventListener('resize', handleWindowResize);
186
+ };
187
+ }, []);
188
+
189
+ // A small helper component for the handle's "grip" icon
190
+ const GripIcon = () => (
191
+ <div
192
+ style={{
193
+ display: 'flex',
194
+ justifyContent: 'center',
195
+ alignItems: 'center',
196
+ height: '100%',
197
+ pointerEvents: 'none',
198
+ }}
199
+ >
200
+ <div
201
+ style={{
202
+ color: 'rgba(0,0,0,0.5)',
203
+ fontSize: '10px',
204
+ lineHeight: '5px',
205
+ userSelect: 'none',
206
+ marginLeft: '1px',
207
+ }}
208
+ >
209
+ ••• •••
210
+ </div>
211
+ </div>
212
+ );
213
+
214
  return (
215
+ <div ref={containerRef} className="w-full h-full flex flex-col relative">
216
  {isPortDropdownOpen && (
217
  <div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
218
  )}
219
  <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
220
  <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
221
+
222
  <div
223
  className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
224
  focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
 
242
  }}
243
  />
244
  </div>
245
+
246
  {previews.length > 1 && (
247
  <PortDropdown
248
  activePreviewIndex={activePreviewIndex}
 
253
  previews={previews}
254
  />
255
  )}
256
+
257
+ {/* Device mode toggle button */}
258
+ <IconButton
259
+ icon="i-ph:devices"
260
+ onClick={toggleDeviceMode}
261
+ title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
262
+ />
263
+
264
+ {/* Fullscreen toggle button */}
265
+ <IconButton
266
+ icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
267
+ onClick={toggleFullscreen}
268
+ title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
269
+ />
270
  </div>
271
+
272
+ <div className="flex-1 border-t border-bolt-elements-borderColor flex justify-center items-center overflow-auto">
273
+ <div
274
+ style={{
275
+ width: isDeviceModeOn ? `${widthPercent}%` : '100%',
276
+ height: '100%', // Always full height
277
+ overflow: 'visible',
278
+ background: '#fff',
279
+ position: 'relative',
280
+ display: 'flex',
281
+ }}
282
+ >
283
+ {activePreview ? (
284
+ <iframe ref={iframeRef} className="border-none w-full h-full bg-white" src={iframeUrl} allowFullScreen />
285
+ ) : (
286
+ <div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
287
+ )}
288
+
289
+ {isDeviceModeOn && (
290
+ <>
291
+ {/* Left handle */}
292
+ <div
293
+ onMouseDown={(e) => startResizing(e, 'left')}
294
+ style={{
295
+ position: 'absolute',
296
+ top: 0,
297
+ left: 0,
298
+ width: '15px',
299
+ marginLeft: '-15px',
300
+ height: '100%',
301
+ cursor: 'ew-resize',
302
+ background: 'rgba(255,255,255,.2)',
303
+ display: 'flex',
304
+ alignItems: 'center',
305
+ justifyContent: 'center',
306
+ transition: 'background 0.2s',
307
+ userSelect: 'none',
308
+ }}
309
+ onMouseOver={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.5)')}
310
+ onMouseOut={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.2)')}
311
+ title="Drag to resize width"
312
+ >
313
+ <GripIcon />
314
+ </div>
315
+
316
+ {/* Right handle */}
317
+ <div
318
+ onMouseDown={(e) => startResizing(e, 'right')}
319
+ style={{
320
+ position: 'absolute',
321
+ top: 0,
322
+ right: 0,
323
+ width: '15px',
324
+ marginRight: '-15px',
325
+ height: '100%',
326
+ cursor: 'ew-resize',
327
+ background: 'rgba(255,255,255,.2)',
328
+ display: 'flex',
329
+ alignItems: 'center',
330
+ justifyContent: 'center',
331
+ transition: 'background 0.2s',
332
+ userSelect: 'none',
333
+ }}
334
+ onMouseOver={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.5)')}
335
+ onMouseOut={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.2)')}
336
+ title="Drag to resize width"
337
+ >
338
+ <GripIcon />
339
+ </div>
340
+ </>
341
+ )}
342
+ </div>
343
  </div>
344
  </div>
345
  );