1- import { PropsWithChildren } from 'react' ;
1+ import { PropsWithChildren , useEffect , useRef , useState } from 'react' ;
22import { useTranslation } from 'react-i18next' ;
3+ import {
4+ ImperativePanelHandle ,
5+ Panel ,
6+ PanelGroup ,
7+ PanelResizeHandle ,
8+ } from 'react-resizable-panels' ;
39import { css } from 'styled-components' ;
410
511import { Box } from '@/components' ;
@@ -12,50 +18,186 @@ import { useResponsiveStore } from '@/stores';
1218
1319type MainLayoutProps = {
1420 backgroundColor ?: 'white' | 'grey' ;
21+ enableResize ?: boolean ;
1522} ;
1623
1724export function MainLayout ( {
1825 children,
1926 backgroundColor = 'white' ,
27+ enableResize = true ,
2028} : PropsWithChildren < MainLayoutProps > ) {
2129 const { isDesktop } = useResponsiveStore ( ) ;
2230 const { colorsTokens } = useCunninghamTheme ( ) ;
2331 const currentBackgroundColor = ! isDesktop ? 'white' : backgroundColor ;
2432 const { t } = useTranslation ( ) ;
2533
34+ // Convert a target pixel width to a percentage of the current viewport width.
35+ // react-resizable-panels expects sizes in %, not px.
36+ const calculateDefaultSize = (
37+ targetWidth : number ,
38+ isDesktopDevice : boolean ,
39+ ) => {
40+ if ( ! isDesktopDevice ) {
41+ return 0 ;
42+ }
43+ const windowWidth = window . innerWidth ;
44+ return ( targetWidth / windowWidth ) * 100 ;
45+ } ;
46+
47+ const ref = useRef < ImperativePanelHandle > ( null ) ;
48+ const [ isResizing , setIsResizing ] = useState ( false ) ;
49+ const resizeTimeoutRef = useRef < number | undefined > ( undefined ) ;
50+ const MIN_PANEL_SIZE = 300 ;
51+ const MAX_PANEL_SIZE = 450 ;
52+
53+ const [ minPanelSize , setMinPanelSize ] = useState (
54+ calculateDefaultSize ( MIN_PANEL_SIZE , isDesktop ) ,
55+ ) ;
56+ const [ maxPanelSize , setMaxPanelSize ] = useState (
57+ calculateDefaultSize ( MAX_PANEL_SIZE , isDesktop ) ,
58+ ) ;
59+
60+ // UX: During window resize, temporarily disable CSS transitions to avoid flicker.
61+ // This does not affect the resize feature; it only improves visual smoothness.
62+ useEffect ( ( ) => {
63+ const handleResizeStart = ( ) => {
64+ setIsResizing ( true ) ;
65+ if ( resizeTimeoutRef . current ) {
66+ clearTimeout ( resizeTimeoutRef . current ) ;
67+ }
68+ resizeTimeoutRef . current = window . setTimeout ( ( ) => {
69+ setIsResizing ( false ) ;
70+ } , 150 ) ;
71+ } ;
72+
73+ window . addEventListener ( 'resize' , handleResizeStart ) ;
74+
75+ return ( ) => {
76+ window . removeEventListener ( 'resize' , handleResizeStart ) ;
77+ if ( resizeTimeoutRef . current ) {
78+ clearTimeout ( resizeTimeoutRef . current ) ;
79+ }
80+ } ;
81+ } , [ ] ) ;
82+
83+ // Keep pixel-based constraints while the library works in percentages.
84+ // We translate px -> % on mount and on viewport resizes so that:
85+ // - min stays ~300px, max stays ~450px (capped to 40% on small screens)
86+ // - on mobile, the left panel collapses (min = 0)
87+ // - when enableResize is false, we lock the size by setting max == min
88+ useEffect ( ( ) => {
89+ const updatePanelSize = ( ) => {
90+ const min = Math . round ( calculateDefaultSize ( MIN_PANEL_SIZE , isDesktop ) ) ;
91+ const max = Math . round (
92+ Math . min ( calculateDefaultSize ( MAX_PANEL_SIZE , isDesktop ) , 40 ) ,
93+ ) ;
94+ setMinPanelSize ( isDesktop ? min : 0 ) ;
95+ enableResize ? setMaxPanelSize ( max ) : setMaxPanelSize ( min ) ;
96+ } ;
97+
98+ updatePanelSize ( ) ;
99+ window . addEventListener ( 'resize' , updatePanelSize ) ;
100+
101+ return ( ) => {
102+ window . removeEventListener ( 'resize' , updatePanelSize ) ;
103+ } ;
104+ } , [ isDesktop , enableResize ] ) ;
105+
26106 return (
27- < Box className = "--docs--main-layout" >
107+ < Box
108+ className = { `--docs--main-layout ${ isResizing ? 'resizing' : '' } ` }
109+ $css = { css `
110+ & .resizing * {
111+ transition : none !important ;
112+ }
113+ ` }
114+ >
28115 < Header />
29116 < Box
30117 $direction = "row"
31118 $margin = { { top : `${ HEADER_HEIGHT } px` } }
32119 $width = "100%"
120+ $height = { `calc(100dvh - ${ HEADER_HEIGHT } px)` }
33121 >
34- < LeftPanel />
35- < Box
36- as = "main"
37- role = "main"
38- aria-label = { t ( 'Main content' ) }
39- id = { MAIN_LAYOUT_ID }
40- $align = "center"
41- $flex = { 1 }
42- $width = "100%"
43- $height = { `calc(100dvh - ${ HEADER_HEIGHT } px)` }
44- $padding = { {
45- all : isDesktop ? 'base' : '0' ,
46- } }
47- $background = {
48- currentBackgroundColor === 'white'
49- ? colorsTokens [ 'greyscale-000' ]
50- : colorsTokens [ 'greyscale-050' ]
51- }
52- $css = { css `
53- overflow-y : auto;
54- overflow-x : clip;
55- ` }
56- >
57- { children }
58- </ Box >
122+ { isDesktop ? (
123+ < PanelGroup
124+ autoSaveId = "docs-left-panel-persistence"
125+ direction = "horizontal"
126+ >
127+ < Panel
128+ ref = { ref }
129+ order = { 0 }
130+ defaultSize = { minPanelSize }
131+ minSize = { minPanelSize }
132+ maxSize = { maxPanelSize }
133+ >
134+ < LeftPanel />
135+ </ Panel >
136+ < PanelResizeHandle
137+ className = "border-clr-surface-primary"
138+ style = { {
139+ borderRightWidth : '1px' ,
140+ borderRightStyle : 'solid' ,
141+ borderRightColor : colorsTokens [ 'greyscale-200' ] ,
142+ width : '1px' ,
143+ cursor : 'col-resize' ,
144+ } }
145+ />
146+ < Panel order = { 1 } >
147+ < Box
148+ as = "main"
149+ role = "main"
150+ aria-label = { t ( 'Main content' ) }
151+ id = { MAIN_LAYOUT_ID }
152+ $align = "center"
153+ $width = "100%"
154+ $height = { `calc(100dvh - ${ HEADER_HEIGHT } px)` }
155+ $padding = { {
156+ all : 'base' ,
157+ } }
158+ $background = {
159+ currentBackgroundColor === 'white'
160+ ? colorsTokens [ 'greyscale-000' ]
161+ : colorsTokens [ 'greyscale-050' ]
162+ }
163+ $css = { css `
164+ overflow-y : auto;
165+ overflow-x : clip;
166+ ` }
167+ >
168+ { children }
169+ </ Box >
170+ </ Panel >
171+ </ PanelGroup >
172+ ) : (
173+ < >
174+ < LeftPanel />
175+ < Box
176+ as = "main"
177+ role = "main"
178+ aria-label = { t ( 'Main content' ) }
179+ id = { MAIN_LAYOUT_ID }
180+ $align = "center"
181+ $flex = { 1 }
182+ $width = "100%"
183+ $height = { `calc(100dvh - ${ HEADER_HEIGHT } px)` }
184+ $padding = { {
185+ all : '0' ,
186+ } }
187+ $background = {
188+ currentBackgroundColor === 'white'
189+ ? colorsTokens [ 'greyscale-000' ]
190+ : colorsTokens [ 'greyscale-050' ]
191+ }
192+ $css = { css `
193+ overflow-y : auto;
194+ overflow-x : clip;
195+ ` }
196+ >
197+ { children }
198+ </ Box >
199+ </ >
200+ ) }
59201 </ Box >
60202 </ Box >
61203 ) ;
0 commit comments