SilentGalaxZy commited on
Commit
a8903ce
·
1 Parent(s): 1890c4e

Added Fullscreen and Resizing to Preview

Browse files
Files changed (1) hide show
  1. app/components/workbench/Preview.tsx +240 -18
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,26 +21,40 @@ export const Preview = memo(() => {
16
  const [url, setUrl] = useState('');
17
  const [iframeUrl, setIframeUrl] = useState<string | undefined>();
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  useEffect(() => {
20
  if (!activePreview) {
21
  setUrl('');
22
  setIframeUrl(undefined);
23
-
24
  return;
25
  }
26
 
27
  const { baseUrl } = activePreview;
28
-
29
  setUrl(baseUrl);
30
  setIframeUrl(baseUrl);
31
- }, [activePreview, iframeUrl]);
32
 
33
  const validateUrl = useCallback(
34
  (value: string) => {
35
- if (!activePreview) {
36
- return false;
37
- }
38
-
39
  const { baseUrl } = activePreview;
40
 
41
  if (value === baseUrl) {
@@ -56,14 +75,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 +89,129 @@ 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 +235,7 @@ export const Preview = memo(() => {
101
  }}
102
  />
103
  </div>
 
104
  {previews.length > 1 && (
105
  <PortDropdown
106
  activePreviewIndex={activePreviewIndex}
@@ -111,13 +246,100 @@ 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
+ // Height is always 100%
30
+ const height = '100%';
31
+
32
+ const resizingState = useRef({
33
+ isResizing: false,
34
+ side: null as ResizeSide,
35
+ startX: 0,
36
+ startWidthPercent: 37.5,
37
+ windowWidth: window.innerWidth,
38
+ });
39
+
40
+ // Define the scaling factor
41
+ const SCALING_FACTOR = 2; // Adjust this value to increase/decrease sensitivity
42
+
43
  useEffect(() => {
44
  if (!activePreview) {
45
  setUrl('');
46
  setIframeUrl(undefined);
 
47
  return;
48
  }
49
 
50
  const { baseUrl } = activePreview;
 
51
  setUrl(baseUrl);
52
  setIframeUrl(baseUrl);
53
+ }, [activePreview]);
54
 
55
  const validateUrl = useCallback(
56
  (value: string) => {
57
+ if (!activePreview) return false;
 
 
 
58
  const { baseUrl } = activePreview;
59
 
60
  if (value === baseUrl) {
 
75
  [],
76
  );
77
 
78
+ // When previews change, display the lowest port if user hasn't selected a preview
79
  useEffect(() => {
80
  if (previews.length > 1 && !hasSelectedPreview.current) {
81
  const minPortIndex = previews.reduce(findMinPortIndex, 0);
 
82
  setActivePreviewIndex(minPortIndex);
83
  }
84
+ }, [previews, findMinPortIndex]);
85
 
86
  const reloadPreview = () => {
87
  if (iframeRef.current) {
 
89
  }
90
  };
91
 
92
+ const toggleFullscreen = async () => {
93
+ if (!isFullscreen && containerRef.current) {
94
+ await containerRef.current.requestFullscreen();
95
+ } else if (document.fullscreenElement) {
96
+ await document.exitFullscreen();
97
+ }
98
+ };
99
+
100
+ useEffect(() => {
101
+ const handleFullscreenChange = () => {
102
+ setIsFullscreen(!!document.fullscreenElement);
103
+ };
104
+
105
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
106
+ return () => {
107
+ document.removeEventListener('fullscreenchange', handleFullscreenChange);
108
+ };
109
+ }, []);
110
+
111
+ const toggleDeviceMode = () => {
112
+ setIsDeviceModeOn((prev) => !prev);
113
+ };
114
+
115
+ const startResizing = (e: React.MouseEvent, side: ResizeSide) => {
116
+ if (!isDeviceModeOn) return;
117
+
118
+ // Prevent text selection
119
+ document.body.style.userSelect = 'none';
120
+
121
+ resizingState.current.isResizing = true;
122
+ resizingState.current.side = side;
123
+ resizingState.current.startX = e.clientX;
124
+ resizingState.current.startWidthPercent = widthPercent;
125
+ resizingState.current.windowWidth = window.innerWidth;
126
+
127
+ document.addEventListener('mousemove', onMouseMove);
128
+ document.addEventListener('mouseup', onMouseUp);
129
+
130
+ e.preventDefault(); // Prevent any text selection on mousedown
131
+ };
132
+
133
+ const onMouseMove = (e: MouseEvent) => {
134
+ if (!resizingState.current.isResizing) return;
135
+ const dx = e.clientX - resizingState.current.startX;
136
+ const windowWidth = resizingState.current.windowWidth;
137
+
138
+ // Apply scaling factor to increase sensitivity
139
+ const dxPercent = ((dx / windowWidth) * 100) * SCALING_FACTOR;
140
+
141
+ let newWidthPercent = resizingState.current.startWidthPercent;
142
+
143
+ if (resizingState.current.side === 'right') {
144
+ newWidthPercent = resizingState.current.startWidthPercent + dxPercent;
145
+ } else if (resizingState.current.side === 'left') {
146
+ newWidthPercent = resizingState.current.startWidthPercent - dxPercent;
147
+ }
148
+
149
+ // Clamp the width between 10% and 90%
150
+ newWidthPercent = Math.max(10, Math.min(newWidthPercent, 90));
151
+
152
+ setWidthPercent(newWidthPercent);
153
+ };
154
+
155
+ const onMouseUp = () => {
156
+ resizingState.current.isResizing = false;
157
+ resizingState.current.side = null;
158
+ document.removeEventListener('mousemove', onMouseMove);
159
+ document.removeEventListener('mouseup', onMouseUp);
160
+
161
+ // Restore text selection
162
+ document.body.style.userSelect = '';
163
+ };
164
+
165
+ // Handle window resize to ensure widthPercent remains valid
166
+ useEffect(() => {
167
+ const handleWindowResize = () => {
168
+ // Optional: Adjust widthPercent if necessary
169
+ // For now, since widthPercent is relative, no action is needed
170
+ };
171
+
172
+ window.addEventListener('resize', handleWindowResize);
173
+ return () => {
174
+ window.removeEventListener('resize', handleWindowResize);
175
+ };
176
+ }, []);
177
+
178
+ // A small helper component for the handle's "grip" icon
179
+ const GripIcon = () => (
180
+ <div
181
+ style={{
182
+ display: 'flex',
183
+ justifyContent: 'center',
184
+ alignItems: 'center',
185
+ height: '100%',
186
+ pointerEvents: 'none',
187
+ }}
188
+ >
189
+ <div
190
+ style={{
191
+ color: 'rgba(0,0,0,0.5)',
192
+ fontSize: '10px',
193
+ lineHeight: '5px',
194
+ userSelect: 'none',
195
+ marginLeft: '1px',
196
+ }}
197
+ >
198
+ •••
199
+ •••
200
+ </div>
201
+ </div>
202
+ );
203
+
204
  return (
205
+ <div ref={containerRef} className="w-full h-full flex flex-col relative">
206
  {isPortDropdownOpen && (
207
+ <div
208
+ className="z-iframe-overlay w-full h-full absolute"
209
+ onClick={() => setIsPortDropdownOpen(false)}
210
+ />
211
  )}
212
  <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
213
  <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
214
+
215
  <div
216
  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
217
  focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
 
235
  }}
236
  />
237
  </div>
238
+
239
  {previews.length > 1 && (
240
  <PortDropdown
241
  activePreviewIndex={activePreviewIndex}
 
246
  previews={previews}
247
  />
248
  )}
249
+
250
+ {/* Device mode toggle button */}
251
+ <IconButton
252
+ icon="i-ph:devices"
253
+ onClick={toggleDeviceMode}
254
+ title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
255
+ />
256
+
257
+ {/* Fullscreen toggle button */}
258
+ <IconButton
259
+ icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
260
+ onClick={toggleFullscreen}
261
+ title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
262
+ />
263
  </div>
264
+
265
+ <div className="flex-1 border-t border-bolt-elements-borderColor flex justify-center items-center overflow-auto">
266
+ <div
267
+ style={{
268
+ width: isDeviceModeOn ? `${widthPercent}%` : '100%',
269
+ height: '100%', // Always full height
270
+ overflow: 'visible',
271
+ background: '#fff',
272
+ position: 'relative',
273
+ display: 'flex',
274
+ }}
275
+ >
276
+ {activePreview ? (
277
+ <iframe
278
+ ref={iframeRef}
279
+ className="border-none w-full h-full bg-white"
280
+ src={iframeUrl}
281
+ allowFullScreen
282
+ />
283
+ ) : (
284
+ <div className="flex w-full h-full justify-center items-center bg-white">
285
+ No preview available
286
+ </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
  );