@@ -3,25 +3,33 @@ import { PROJECT, PROJECT_SETTINGS } from "../project/project";
33import { WithCurrentFrame } from "./lib/frame"
44import { TimelineUI } from "./ui/timeline" ;
55import { ClipVisibilityPanel } from "./ui/clip-visibility" ;
6+ import { CodeEditor } from "./ui/code-editor" ;
7+ import { EditorProvider } from "./ui/editor-context" ;
68import { Store } from "./util/state" ;
79import { StudioStateContext } from "./lib/studio-state"
810
911// Back-compat re-exports (avoid HMR issues if some modules still import these from StudioApp).
1012export { StudioStateContext , useIsPlaying , useIsPlayingStore , useIsRender , useSetIsPlaying } from "./lib/studio-state"
1113
1214export const StudioApp = ( ) => {
15+ const rowRef = useRef < HTMLDivElement > ( null ) ;
1316 const containerRef = useRef < HTMLDivElement > ( null ) ;
1417 const topRef = useRef < HTMLDivElement > ( null ) ;
1518 const previewRef = useRef < HTMLDivElement > ( null ) ;
1619 const [ verticalRatio , setVerticalRatio ] = useState ( 0.6 ) ; // top area height ratio
1720 const [ horizontalRatio , setHorizontalRatio ] = useState ( 0.3 ) ; // clips width ratio within top area
21+ const [ editorWidth , setEditorWidth ] = useState ( 460 ) ;
22+ const [ isEditorVisible , setIsEditorVisible ] = useState ( true ) ;
1823 const projectWidth = PROJECT_SETTINGS . width || 1920
1924 const projectHeight = PROJECT_SETTINGS . height || 1080
2025 const previewAspect = `${ projectWidth } / ${ projectHeight } `
2126 const previewAspectValue = projectHeight / projectWidth
2227 const previewMinWidth = 320 ;
2328 const previewMinHeight = previewMinWidth * previewAspectValue ;
2429 const timelineMinHeight = 200 ;
30+ const leftPanelMinWidth = previewMinWidth + 220 + 6 + 20 ;
31+ const editorMinWidth = 320 ;
32+ const rowGap = 10 ;
2533 const [ previewViewport , setPreviewViewport ] = useState < { width : number ; height : number } > ( { width : 0 , height : 0 } ) ;
2634 const hasPreviewViewport = previewViewport . width > 0 && previewViewport . height > 0 ;
2735
@@ -132,108 +140,167 @@ export const StudioApp = () => {
132140 [ isPlayingStore ]
133141 ) ;
134142
143+ const clampEditorWidth = useCallback (
144+ ( nextWidth : number ) => {
145+ const row = rowRef . current ;
146+ if ( ! row ) {
147+ setEditorWidth ( Math . max ( editorMinWidth , nextWidth ) ) ;
148+ return ;
149+ }
150+ const maxWidth = Math . max ( editorMinWidth , row . clientWidth - rowGap - leftPanelMinWidth ) ;
151+ const clamped = Math . min ( Math . max ( nextWidth , editorMinWidth ) , maxWidth ) ;
152+ setEditorWidth ( clamped ) ;
153+ } ,
154+ [ editorMinWidth , leftPanelMinWidth , rowGap ] ,
155+ ) ;
156+
157+ useEffect ( ( ) => {
158+ const row = rowRef . current ;
159+ if ( ! row ) return ;
160+ const observer = new ResizeObserver ( ( ) => {
161+ setEditorWidth ( ( prev ) => {
162+ const maxWidth = Math . max ( editorMinWidth , row . clientWidth - rowGap - leftPanelMinWidth ) ;
163+ return Math . min ( prev , maxWidth ) ;
164+ } ) ;
165+ } ) ;
166+ observer . observe ( row ) ;
167+ return ( ) => observer . disconnect ( ) ;
168+ } , [ editorMinWidth , leftPanelMinWidth , rowGap ] ) ;
169+
135170 return (
136- < StudioStateContext value = { { isPlaying, setIsPlaying, isPlayingStore, isRender : false } } >
137- < WithCurrentFrame >
138- < div style = { { padding : 16 , height : "100vh" , boxSizing : "border-box" , minHeight : 0 } } >
139- < div
140- ref = { containerRef }
141- style = { {
142- display : "flex" ,
143- flexDirection : "column" ,
144- gap : 10 ,
145- width : "100%" ,
146- height : "100%" ,
147- boxSizing : "border-box" ,
148- minHeight : 0 ,
149- } }
150- >
151- < div
152- ref = { topRef }
153- style = { {
154- display : "flex" ,
155- gap : 10 ,
156- alignItems : "stretch" ,
157- width : "100%" ,
158- flexBasis : `${ verticalRatio * 100 } %` ,
159- minHeight : 240 ,
160- maxHeight : "80%" ,
161- minWidth : 0 ,
162- } }
163- >
164- < div style = { { flexBasis : `${ horizontalRatio * 100 } %` , minWidth : 220 } } >
165- < ClipVisibilityPanel />
166- </ div >
167- < div
168- onPointerDown = { startHorizontalDrag }
169- style = { {
170- width : 6 ,
171- cursor : "col-resize" ,
172- background : "linear-gradient(180deg, #1f2937, #111827)" ,
173- borderRadius : 4 ,
174- flexShrink : 0 ,
175- } }
176- />
177- < div style = { { flex : 1 , minWidth : 320 , display : "flex" , alignItems : "center" , justifyContent : "center" , minHeight : previewMinHeight , position : "relative" } } >
171+ < EditorProvider >
172+ < StudioStateContext value = { { isPlaying, setIsPlaying, isPlayingStore, isRender : false } } >
173+ < WithCurrentFrame >
174+ < div style = { { padding : 16 , height : "100vh" , boxSizing : "border-box" , minHeight : 0 } } >
175+ < div ref = { rowRef } style = { { display : "flex" , gap : 10 , height : "100%" , minHeight : 0 } } >
176+ < div style = { { flex : 1 , minWidth : leftPanelMinWidth , display : "flex" , flexDirection : "column" , minHeight : 0 } } >
177+ < div style = { { display : "flex" , justifyContent : "flex-end" , marginBottom : 8 } } >
178+ < button
179+ type = "button"
180+ onClick = { ( ) => setIsEditorVisible ( ( prev ) => ! prev ) }
181+ style = { {
182+ padding : "6px 10px" ,
183+ borderRadius : 6 ,
184+ border : "1px solid #1f2937" ,
185+ background : "#0f172a" ,
186+ color : "#cbd5e1" ,
187+ cursor : "pointer" ,
188+ fontSize : 12 ,
189+ fontWeight : 600 ,
190+ } }
191+ >
192+ { isEditorVisible ? "Hide Editor" : "Show Editor" }
193+ </ button >
194+ </ div >
195+
178196 < div
179- ref = { previewRef }
197+ ref = { containerRef }
180198 style = { {
199+ display : "flex" ,
200+ flexDirection : "column" ,
201+ gap : 10 ,
181202 width : "100%" ,
182203 height : "100%" ,
183- display : "flex" ,
184- alignItems : "center" ,
185- justifyContent : "center" ,
186204 boxSizing : "border-box" ,
205+ minHeight : 0 ,
206+ flex : 1 ,
187207 } }
188208 >
189209 < div
210+ ref = { topRef }
190211 style = { {
191- width : scaledWidth ,
192- height : scaledHeight ,
193- visibility : hasPreviewViewport ? "visible" : "hidden" ,
194- aspectRatio : previewAspect ,
195- border : "1px solid #444" ,
196- borderRadius : 1 ,
197- overflow : "hidden" ,
198- backgroundColor : "#000" ,
199- boxShadow : "0 10px 30px rgba(0,0,0,0.35)" ,
200- position : "relative" ,
212+ display : "flex" ,
213+ gap : 10 ,
214+ alignItems : "stretch" ,
215+ width : "100%" ,
216+ flexBasis : `${ verticalRatio * 100 } %` ,
217+ minHeight : 240 ,
218+ maxHeight : "80%" ,
219+ minWidth : 0 ,
201220 } }
202221 >
222+ < div style = { { flexBasis : `${ horizontalRatio * 100 } %` , minWidth : 220 } } >
223+ < ClipVisibilityPanel />
224+ </ div >
203225 < div
226+ onPointerDown = { startHorizontalDrag }
204227 style = { {
205- width : projectWidth ,
206- height : projectHeight ,
207- transform : `scale(${ scale } )` ,
208- transformOrigin : "top left" ,
228+ width : 6 ,
229+ cursor : "col-resize" ,
230+ background : "linear-gradient(180deg, #1f2937, #111827)" ,
231+ borderRadius : 4 ,
232+ flexShrink : 0 ,
209233 } }
210- >
211- < PROJECT />
234+ />
235+ < div style = { { flex : 1 , minWidth : 320 , display : "flex" , alignItems : "center" , justifyContent : "center" , minHeight : previewMinHeight , position : "relative" } } >
236+ < div
237+ ref = { previewRef }
238+ style = { {
239+ width : "100%" ,
240+ height : "100%" ,
241+ display : "flex" ,
242+ alignItems : "center" ,
243+ justifyContent : "center" ,
244+ boxSizing : "border-box" ,
245+ } }
246+ >
247+ < div
248+ style = { {
249+ width : scaledWidth ,
250+ height : scaledHeight ,
251+ visibility : hasPreviewViewport ? "visible" : "hidden" ,
252+ aspectRatio : previewAspect ,
253+ border : "1px solid #444" ,
254+ borderRadius : 1 ,
255+ overflow : "hidden" ,
256+ backgroundColor : "#000" ,
257+ boxShadow : "0 10px 30px rgba(0,0,0,0.35)" ,
258+ position : "relative" ,
259+ } }
260+ >
261+ < div
262+ style = { {
263+ width : projectWidth ,
264+ height : projectHeight ,
265+ transform : `scale(${ scale } )` ,
266+ transformOrigin : "top left" ,
267+ } }
268+ >
269+ < PROJECT />
270+ </ div >
271+ </ div >
272+ </ div >
273+ </ div >
274+ </ div >
275+
276+ < div
277+ onPointerDown = { startVerticalDrag }
278+ style = { {
279+ height : 8 ,
280+ cursor : "row-resize" ,
281+ background : "linear-gradient(90deg, #1f2937, #111827)" ,
282+ borderRadius : 4 ,
283+ flexShrink : 0 ,
284+ } }
285+ />
286+
287+ < div style = { { flex : 1 , minHeight : 160 , display : "flex" , minWidth : 0 } } >
288+ < div style = { { flex : 1 , minHeight : 0 } } >
289+ < TimelineUI />
212290 </ div >
213291 </ div >
214292 </ div >
215293 </ div >
216- </ div >
217294
218- < div
219- onPointerDown = { startVerticalDrag }
220- style = { {
221- height : 8 ,
222- cursor : "row-resize" ,
223- background : "linear-gradient(90deg, #1f2937, #111827)" ,
224- borderRadius : 4 ,
225- flexShrink : 0 ,
226- } }
227- />
228-
229- < div style = { { flex : 1 , minHeight : 160 , display : "flex" , minWidth : 0 } } >
230- < div style = { { flex : 1 , minHeight : 0 } } >
231- < TimelineUI />
232- </ div >
295+ { isEditorVisible ? (
296+ < div style = { { width : editorWidth , minWidth : editorMinWidth , height : "100%" , minHeight : 0 , display : "flex" } } >
297+ < CodeEditor width = { editorWidth } onWidthChange = { clampEditorWidth } />
298+ </ div >
299+ ) : null }
233300 </ div >
234301 </ div >
235- </ div >
236- </ WithCurrentFrame >
237- </ StudioStateContext >
302+ </ WithCurrentFrame >
303+ </ StudioStateContext >
304+ </ EditorProvider >
238305 ) ;
239306} ;
0 commit comments