Skip to content

Commit 8cc45af

Browse files
committed
Add ability to resize Code panel
1 parent 65f914e commit 8cc45af

File tree

6 files changed

+221
-19
lines changed

6 files changed

+221
-19
lines changed

src/App.tsx

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { pythonGenerator } from 'blockly/python';
2828
import Header from './reactComponents/Header';
2929
import * as Menu from './reactComponents/Menu';
3030
import CodeDisplay from './reactComponents/CodeDisplay';
31+
import SiderCollapseTrigger from './reactComponents/SiderCollapseTrigger';
3132
import BlocklyComponent, { BlocklyComponentType } from './reactComponents/BlocklyComponent';
3233
import ToolboxSettingsModal from './reactComponents/ToolboxSettings';
3334
import * as Tabs from './reactComponents/Tabs';
@@ -78,10 +79,10 @@ const FULL_VIEWPORT_HEIGHT = '100vh';
7879
const FULL_HEIGHT = '100%';
7980

8081
/** Default size for code panel. */
81-
const CODE_PANEL_DEFAULT_SIZE = '25%';
82+
const CODE_PANEL_DEFAULT_SIZE = 400;
8283

8384
/** Minimum size for code panel. */
84-
const CODE_PANEL_MIN_SIZE = 80;
85+
const CODE_PANEL_MIN_SIZE = 100;
8586

8687
/** Background color for testing layout. */
8788
const LAYOUT_BACKGROUND_COLOR = '#0F0';
@@ -166,7 +167,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
166167
const [shownPythonToolboxCategories, setShownPythonToolboxCategories] = React.useState<Set<string>>(new Set());
167168
const [triggerPythonRegeneration, setTriggerPythonRegeneration] = React.useState(0);
168169
const [leftCollapsed, setLeftCollapsed] = React.useState(false);
169-
const [rightCollapsed, setRightCollapsed] = React.useState(false);
170+
const [codePanelSize, setCodePanelSize] = React.useState<number>(CODE_PANEL_DEFAULT_SIZE);
171+
const [codePanelCollapsed, setCodePanelCollapsed] = React.useState(false);
172+
const [codePanelExpandedSize, setCodePanelExpandedSize] = React.useState<number>(CODE_PANEL_DEFAULT_SIZE);
173+
const [codePanelAnimating, setCodePanelAnimating] = React.useState(false);
170174
const [theme, setTheme] = React.useState('dark');
171175
const [languageInitialized, setLanguageInitialized] = React.useState(false);
172176
const [themeInitialized, setThemeInitialized] = React.useState(false);
@@ -378,6 +382,27 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
378382
setToolboxSettingsModalIsOpen(false);
379383
};
380384

385+
/** Toggles the code panel between collapsed and expanded states. */
386+
const toggleCodePanelCollapse = (): void => {
387+
setCodePanelAnimating(true);
388+
389+
if (codePanelCollapsed) {
390+
// Expand to previous size
391+
setCodePanelSize(codePanelExpandedSize);
392+
setCodePanelCollapsed(false);
393+
} else {
394+
// Collapse to minimum size
395+
setCodePanelExpandedSize(codePanelSize);
396+
setCodePanelSize(CODE_PANEL_MIN_SIZE);
397+
setCodePanelCollapsed(true);
398+
}
399+
400+
// Reset animation flag after transition completes
401+
setTimeout(() => {
402+
setCodePanelAnimating(false);
403+
}, 200);
404+
};
405+
381406
/** Handles toolbox settings modal OK with updated categories. */
382407
const handleToolboxSettingsConfirm = (updatedShownCategories: Set<string>): void => {
383408
setToolboxSettingsModalIsOpen(false);
@@ -618,6 +643,8 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
618643
collapsible
619644
collapsed={leftCollapsed}
620645
onCollapse={(collapsed: boolean) => setLeftCollapsed(collapsed)}
646+
trigger={null}
647+
style={{ position: 'relative' }}
621648
>
622649
<Menu.Component
623650
storage={storage}
@@ -629,6 +656,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
629656
theme={theme}
630657
setTheme={setTheme}
631658
/>
659+
<SiderCollapseTrigger
660+
collapsed={leftCollapsed}
661+
onToggle={() => setLeftCollapsed(!leftCollapsed)}
662+
/>
632663
</Sider>
633664
<Antd.Layout>
634665
<Tabs.Component
@@ -642,8 +673,8 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
642673
setProject={setProject}
643674
storage={storage}
644675
/>
645-
<Antd.Layout>
646-
<Content>
676+
<div style={{ display: 'flex', height: FULL_HEIGHT }}>
677+
<Content style={{ flex: 1, height: '100%' }}>
647678
{modulePaths.current.map((modulePath) => (
648679
<BlocklyComponent
649680
key={modulePath}
@@ -654,22 +685,65 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
654685
/>
655686
))}
656687
</Content>
657-
<Sider
658-
collapsible
659-
reverseArrow={true}
660-
collapsed={rightCollapsed}
661-
collapsedWidth={CODE_PANEL_MIN_SIZE}
662-
width={CODE_PANEL_DEFAULT_SIZE}
663-
onCollapse={(collapsed: boolean) => setRightCollapsed(collapsed)}
688+
<div
689+
style={{
690+
width: `${codePanelSize}px`,
691+
minWidth: CODE_PANEL_MIN_SIZE,
692+
height: '100%',
693+
borderLeft: '1px solid #d9d9d9',
694+
position: 'relative',
695+
transition: codePanelAnimating ? 'width 0.2s ease' : 'none'
696+
}}
664697
>
698+
<div
699+
style={{
700+
position: 'absolute',
701+
left: 0,
702+
top: 0,
703+
width: '4px',
704+
height: '100%',
705+
cursor: 'ew-resize',
706+
backgroundColor: 'transparent',
707+
zIndex: 10,
708+
transform: 'translateX(-2px)'
709+
}}
710+
onMouseDown={(e) => {
711+
e.preventDefault();
712+
const startX = e.clientX;
713+
const startWidth = codePanelSize;
714+
715+
const handleMouseMove = (e: MouseEvent) => {
716+
const deltaX = startX - e.clientX;
717+
const newWidth = Math.max(CODE_PANEL_MIN_SIZE, startWidth + deltaX);
718+
setCodePanelSize(newWidth);
719+
// Update expanded size if not at minimum
720+
if (newWidth > CODE_PANEL_MIN_SIZE) {
721+
setCodePanelExpandedSize(newWidth);
722+
setCodePanelCollapsed(false);
723+
} else {
724+
setCodePanelCollapsed(true);
725+
}
726+
};
727+
728+
const handleMouseUp = () => {
729+
document.removeEventListener('mousemove', handleMouseMove);
730+
document.removeEventListener('mouseup', handleMouseUp);
731+
};
732+
733+
document.addEventListener('mousemove', handleMouseMove);
734+
document.addEventListener('mouseup', handleMouseUp);
735+
}}
736+
/>
665737
<CodeDisplay
666738
generatedCode={generatedCode}
667739
messageApi={messageApi}
668740
setAlertErrorMessage={setAlertErrorMessage}
669741
theme={theme}
742+
isCollapsed={codePanelCollapsed}
743+
onToggleCollapse={toggleCodePanelCollapse}
670744
/>
671-
</Sider>
672-
</Antd.Layout>
745+
</div>
746+
</div>
673747
</Antd.Layout>
674748
</Antd.Layout>
675749
</Antd.Layout>

src/i18n/locales/en/translation.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"BLOCKS": "Blocks",
4444
"CODE": "Code",
4545
"COPY": "Copy",
46+
"COLLAPSE": "Collapse",
47+
"EXPAND": "Expand",
4648
"FAILED_TO_RENAME_PROJECT": "Failed to rename project",
4749
"FAILED_TO_COPY_PROJECT": "Failed to copy project",
4850
"FAILED_TO_CREATE_PROJECT": "Failed to create a new project.",

src/i18n/locales/es/translation.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
"BLOCKS": "Bloques",
4141
"CODE": "Código",
4242
"COPY": "Copiar",
43+
"COLLAPSE": "Colapsar",
44+
"EXPAND": "Expandir",
4345
"FAILED_TO_RENAME_PROJECT": "Error al renombrar proyecto",
4446
"FAILED_TO_COPY_PROJECT": "Error al copiar proyecto",
4547
"FAILED_TO_CREATE_PROJECT": "Error al crear un nuevo proyecto.",

src/i18n/locales/he/translation.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"BLOCKS": "בלוקים",
4444
"CODE": "קוד",
4545
"COPY": "העתק",
46+
"COLLAPSE": "כווץ",
47+
"EXPAND": "הרחב",
4648
"FAILED_TO_RENAME_PROJECT": "נכשל בשינוי שם הפרויקט",
4749
"FAILED_TO_COPY_PROJECT": "נכשל בהעתקת הפרויקט",
4850
"FAILED_TO_CREATE_PROJECT": "נכשל ביצירת פרויקט חדש.",

src/reactComponents/CodeDisplay.tsx

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*/
2121
import * as Antd from 'antd';
2222
import * as React from 'react';
23-
import { CopyOutlined as CopyIcon } from '@ant-design/icons';
23+
import { CopyOutlined as CopyIcon, LeftOutlined as CollapseIcon, RightOutlined as ExpandIcon } from '@ant-design/icons';
2424
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
2525
import { dracula, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
2626

@@ -36,6 +36,8 @@ interface CodeDisplayProps {
3636
theme: string;
3737
messageApi: MessageInstance;
3838
setAlertErrorMessage: StringFunction;
39+
isCollapsed?: boolean;
40+
onToggleCollapse?: () => void;
3941
}
4042

4143
/** Success message for copy operation. */
@@ -110,10 +112,56 @@ export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element
110112
</SyntaxHighlighter>
111113
);
112114

115+
/** Renders the collapse/expand trigger at the bottom center of the panel. */
116+
const renderCollapseTrigger = (): React.JSX.Element | null => {
117+
if (!props.onToggleCollapse) return null;
118+
119+
return (
120+
<div
121+
style={{
122+
position: 'absolute',
123+
bottom: 0,
124+
left: '50%',
125+
transform: 'translateX(-50%)',
126+
backgroundColor: token.colorBgContainer,
127+
border: `1px solid ${token.colorBorder}`,
128+
borderBottom: 'none',
129+
borderRadius: '6px 6px 0 0',
130+
padding: '2px 6px',
131+
cursor: 'pointer',
132+
display: 'flex',
133+
alignItems: 'center',
134+
justifyContent: 'center',
135+
minWidth: '24px',
136+
height: '22px',
137+
color: token.colorTextSecondary,
138+
transition: 'all 0.2s',
139+
zIndex: 1,
140+
}}
141+
onClick={props.onToggleCollapse}
142+
onMouseEnter={(e) => {
143+
e.currentTarget.style.color = token.colorText;
144+
e.currentTarget.style.backgroundColor = token.colorBgTextHover;
145+
}}
146+
onMouseLeave={(e) => {
147+
e.currentTarget.style.color = token.colorTextSecondary;
148+
e.currentTarget.style.backgroundColor = token.colorBgContainer;
149+
}}
150+
>
151+
<Antd.Tooltip title={props.isCollapsed ? t("EXPAND") : t("COLLAPSE")}>
152+
{props.isCollapsed ? <ExpandIcon /> : <CollapseIcon />}
153+
</Antd.Tooltip>
154+
</div>
155+
);
156+
};
157+
113158
return (
114-
<Antd.Flex vertical gap="small" style={{ height: '100%' }}>
115-
{renderHeader()}
116-
{renderCodeBlock()}
117-
</Antd.Flex>
159+
<div style={{ height: '100%', position: 'relative' }}>
160+
<Antd.Flex vertical gap="small" style={{ height: '100%' }}>
161+
{renderHeader()}
162+
{renderCodeBlock()}
163+
</Antd.Flex>
164+
{renderCollapseTrigger()}
165+
</div>
118166
);
119167
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Porpoiseful LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* @author [email protected] (Alan Smith)
20+
*/
21+
import * as React from 'react';
22+
import * as Antd from 'antd';
23+
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
24+
import { useTranslation } from 'react-i18next';
25+
26+
/** Props for the SiderCollapseTrigger component. */
27+
interface SiderCollapseTriggerProps {
28+
collapsed: boolean;
29+
onToggle: () => void;
30+
}
31+
32+
/**
33+
* Custom collapse trigger for Sider that matches the right panel's appearance.
34+
*/
35+
export default function SiderCollapseTrigger(props: SiderCollapseTriggerProps): React.JSX.Element {
36+
const { token } = Antd.theme.useToken();
37+
const { t } = useTranslation();
38+
const [isHovered, setIsHovered] = React.useState(false);
39+
40+
return (
41+
<div
42+
style={{
43+
position: 'absolute',
44+
bottom: 0,
45+
left: '50%',
46+
transform: 'translateX(-50%)',
47+
backgroundColor: isHovered ? token.colorBgTextHover : token.colorBgContainer,
48+
border: `1px solid ${token.colorBorder}`,
49+
borderBottom: 'none',
50+
borderRadius: '6px 6px 0 0',
51+
padding: '2px 6px',
52+
cursor: 'pointer',
53+
display: 'flex',
54+
alignItems: 'center',
55+
justifyContent: 'center',
56+
minWidth: '24px',
57+
height: '22px',
58+
color: isHovered ? token.colorText : token.colorTextSecondary,
59+
transition: 'all 0.2s',
60+
zIndex: 1,
61+
}}
62+
onClick={props.onToggle}
63+
onMouseEnter={() => setIsHovered(true)}
64+
onMouseLeave={() => setIsHovered(false)}
65+
>
66+
<Antd.Tooltip title={props.collapsed ? t("EXPAND") : t("COLLAPSE")}>
67+
{props.collapsed ?
68+
<RightOutlined style={{ fontSize: '12px', color: 'inherit' }} /> :
69+
<LeftOutlined style={{ fontSize: '12px', color: 'inherit' }} />
70+
}
71+
</Antd.Tooltip>
72+
</div>
73+
);
74+
}

0 commit comments

Comments
 (0)