Skip to content

Commit f002bce

Browse files
authored
Allow code panel to be resized (#261)
* Add ability to resize Code panel * Change to use the new collapse committment in the code display * Change to make default starting size a percentage * Reverse arrow on right panel so it makes logical sense
1 parent 457b477 commit f002bce

File tree

6 files changed

+205
-17
lines changed

6 files changed

+205
-17
lines changed

src/App.tsx

Lines changed: 94 additions & 13 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';
@@ -81,7 +82,7 @@ const FULL_HEIGHT = '100%';
8182
const CODE_PANEL_DEFAULT_SIZE = '25%';
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';
@@ -167,7 +168,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
167168
const [shownPythonToolboxCategories, setShownPythonToolboxCategories] = React.useState<Set<string>>(new Set());
168169
const [triggerPythonRegeneration, setTriggerPythonRegeneration] = React.useState(0);
169170
const [leftCollapsed, setLeftCollapsed] = React.useState(false);
170-
const [rightCollapsed, setRightCollapsed] = React.useState(false);
171+
const [codePanelSize, setCodePanelSize] = React.useState<string | number>(CODE_PANEL_DEFAULT_SIZE);
172+
const [codePanelCollapsed, setCodePanelCollapsed] = React.useState(false);
173+
const [codePanelExpandedSize, setCodePanelExpandedSize] = React.useState<string | number>(CODE_PANEL_DEFAULT_SIZE);
174+
const [codePanelAnimating, setCodePanelAnimating] = React.useState(false);
171175
const [theme, setTheme] = React.useState('dark');
172176
const [languageInitialized, setLanguageInitialized] = React.useState(false);
173177
const [themeInitialized, setThemeInitialized] = React.useState(false);
@@ -379,6 +383,30 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
379383
setToolboxSettingsModalIsOpen(false);
380384
};
381385

386+
/** Toggles the code panel between collapsed and expanded states. */
387+
const toggleCodePanelCollapse = (): void => {
388+
setCodePanelAnimating(true);
389+
390+
if (codePanelCollapsed) {
391+
// Expand to previous size
392+
setCodePanelSize(codePanelExpandedSize);
393+
setCodePanelCollapsed(false);
394+
} else {
395+
// Collapse to minimum size - convert current size to pixels for storage
396+
const currentSizePx = typeof codePanelSize === 'string'
397+
? (parseFloat(codePanelSize) / 100) * window.innerWidth
398+
: codePanelSize;
399+
setCodePanelExpandedSize(currentSizePx);
400+
setCodePanelSize(CODE_PANEL_MIN_SIZE);
401+
setCodePanelCollapsed(true);
402+
}
403+
404+
// Reset animation flag after transition completes
405+
setTimeout(() => {
406+
setCodePanelAnimating(false);
407+
}, 200);
408+
};
409+
382410
/** Handles toolbox settings modal OK with updated categories. */
383411
const handleToolboxSettingsConfirm = (updatedShownCategories: Set<string>): void => {
384412
setToolboxSettingsModalIsOpen(false);
@@ -733,6 +761,8 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
733761
collapsible
734762
collapsed={leftCollapsed}
735763
onCollapse={(collapsed: boolean) => setLeftCollapsed(collapsed)}
764+
trigger={null}
765+
style={{ position: 'relative' }}
736766
>
737767
<Menu.Component
738768
storage={storage}
@@ -744,6 +774,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
744774
theme={theme}
745775
setTheme={setTheme}
746776
/>
777+
<SiderCollapseTrigger
778+
collapsed={leftCollapsed}
779+
onToggle={() => setLeftCollapsed(!leftCollapsed)}
780+
/>
747781
</Sider>
748782
<Antd.Layout>
749783
<Tabs.Component
@@ -757,8 +791,8 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
757791
setProject={setProject}
758792
storage={storage}
759793
/>
760-
<Antd.Layout>
761-
<Content>
794+
<div style={{ display: 'flex', height: FULL_HEIGHT }}>
795+
<Content style={{ flex: 1, height: '100%' }}>
762796
{modulePaths.current.map((modulePath) => (
763797
<BlocklyComponent
764798
key={modulePath}
@@ -769,22 +803,69 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
769803
/>
770804
))}
771805
</Content>
772-
<Sider
773-
collapsible
774-
reverseArrow={true}
775-
collapsed={rightCollapsed}
776-
collapsedWidth={CODE_PANEL_MIN_SIZE}
777-
width={CODE_PANEL_DEFAULT_SIZE}
778-
onCollapse={(collapsed: boolean) => setRightCollapsed(collapsed)}
806+
<div
807+
style={{
808+
width: typeof codePanelSize === 'string' ? codePanelSize : `${codePanelSize}px`,
809+
minWidth: CODE_PANEL_MIN_SIZE,
810+
height: '100%',
811+
borderLeft: '1px solid #d9d9d9',
812+
position: 'relative',
813+
transition: codePanelAnimating ? 'width 0.2s ease' : 'none'
814+
}}
779815
>
816+
<div
817+
style={{
818+
position: 'absolute',
819+
left: 0,
820+
top: 0,
821+
width: '4px',
822+
height: '100%',
823+
cursor: 'ew-resize',
824+
backgroundColor: 'transparent',
825+
zIndex: 10,
826+
transform: 'translateX(-2px)'
827+
}}
828+
onMouseDown={(e) => {
829+
e.preventDefault();
830+
const startX = e.clientX;
831+
const startWidth = codePanelSize;
832+
833+
const handleMouseMove = (e: MouseEvent) => {
834+
const deltaX = startX - e.clientX;
835+
// Convert startWidth to number if it's a percentage
836+
const startWidthPx = typeof startWidth === 'string'
837+
? (parseFloat(startWidth) / 100) * window.innerWidth
838+
: startWidth;
839+
const newWidth = Math.max(CODE_PANEL_MIN_SIZE, startWidthPx + deltaX);
840+
setCodePanelSize(newWidth);
841+
// Update expanded size if not at minimum
842+
if (newWidth > CODE_PANEL_MIN_SIZE) {
843+
setCodePanelExpandedSize(newWidth);
844+
setCodePanelCollapsed(false);
845+
} else {
846+
setCodePanelCollapsed(true);
847+
}
848+
};
849+
850+
const handleMouseUp = () => {
851+
document.removeEventListener('mousemove', handleMouseMove);
852+
document.removeEventListener('mouseup', handleMouseUp);
853+
};
854+
855+
document.addEventListener('mousemove', handleMouseMove);
856+
document.addEventListener('mouseup', handleMouseUp);
857+
}}
858+
/>
780859
<CodeDisplay
781860
generatedCode={generatedCode}
782861
messageApi={messageApi}
783862
setAlertErrorMessage={setAlertErrorMessage}
784863
theme={theme}
864+
isCollapsed={codePanelCollapsed}
865+
onToggleCollapse={toggleCodePanelCollapse}
785866
/>
786-
</Sider>
787-
</Antd.Layout>
867+
</div>
868+
</div>
788869
</Antd.Layout>
789870
</Antd.Layout>
790871
</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: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as Antd from 'antd';
2222
import * as React from 'react';
2323
import { CopyOutlined as CopyIcon } from '@ant-design/icons';
2424
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
25+
import SiderCollapseTrigger from './SiderCollapseTrigger';
2526
import { dracula, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
2627

2728
import type { MessageInstance } from 'antd/es/message/interface';
@@ -36,6 +37,8 @@ interface CodeDisplayProps {
3637
theme: string;
3738
messageApi: MessageInstance;
3839
setAlertErrorMessage: StringFunction;
40+
isCollapsed?: boolean;
41+
onToggleCollapse?: () => void;
3942
}
4043

4144
/** Success message for copy operation. */
@@ -110,10 +113,26 @@ export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element
110113
</SyntaxHighlighter>
111114
);
112115

116+
/** Renders the collapse/expand trigger at the bottom center of the panel. */
117+
const renderCollapseTrigger = (): React.JSX.Element | null => {
118+
if (!props.onToggleCollapse) return null;
119+
120+
return (
121+
<SiderCollapseTrigger
122+
collapsed={props.isCollapsed || false}
123+
onToggle={props.onToggleCollapse}
124+
isRightPanel={true}
125+
/>
126+
);
127+
};
128+
113129
return (
114-
<Antd.Flex vertical gap="small" style={{ height: '100%' }}>
115-
{renderHeader()}
116-
{renderCodeBlock()}
117-
</Antd.Flex>
130+
<div style={{ height: '100%', position: 'relative' }}>
131+
<Antd.Flex vertical gap="small" style={{ height: '100%' }}>
132+
{renderHeader()}
133+
{renderCodeBlock()}
134+
</Antd.Flex>
135+
{renderCollapseTrigger()}
136+
</div>
118137
);
119138
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
isRightPanel?: boolean;
31+
}
32+
33+
/**
34+
* Custom collapse trigger for Sider that matches the right panel's appearance.
35+
*/
36+
export default function SiderCollapseTrigger(props: SiderCollapseTriggerProps): React.JSX.Element {
37+
const { token } = Antd.theme.useToken();
38+
const { t } = useTranslation();
39+
const [isHovered, setIsHovered] = React.useState(false);
40+
41+
return (
42+
<div
43+
style={{
44+
position: 'absolute',
45+
bottom: 0,
46+
left: '50%',
47+
transform: 'translateX(-50%)',
48+
backgroundColor: isHovered ? token.colorBgTextHover : token.colorBgContainer,
49+
border: `1px solid ${token.colorBorder}`,
50+
borderBottom: 'none',
51+
borderRadius: '6px 6px 0 0',
52+
padding: '2px 6px',
53+
cursor: 'pointer',
54+
display: 'flex',
55+
alignItems: 'center',
56+
justifyContent: 'center',
57+
minWidth: '24px',
58+
height: '22px',
59+
color: isHovered ? token.colorText : token.colorTextSecondary,
60+
transition: 'all 0.2s',
61+
zIndex: 1,
62+
}}
63+
onClick={props.onToggle}
64+
onMouseEnter={() => setIsHovered(true)}
65+
onMouseLeave={() => setIsHovered(false)}
66+
>
67+
<Antd.Tooltip title={props.collapsed ? t("EXPAND") : t("COLLAPSE")}>
68+
{props.isRightPanel ? (
69+
// Right panel: reversed arrows
70+
props.collapsed ?
71+
<LeftOutlined style={{ fontSize: '12px', color: 'inherit' }} /> :
72+
<RightOutlined style={{ fontSize: '12px', color: 'inherit' }} />
73+
) : (
74+
// Left panel: normal arrows
75+
props.collapsed ?
76+
<RightOutlined style={{ fontSize: '12px', color: 'inherit' }} /> :
77+
<LeftOutlined style={{ fontSize: '12px', color: 'inherit' }} />
78+
)}
79+
</Antd.Tooltip>
80+
</div>
81+
);
82+
}

0 commit comments

Comments
 (0)