From 8cc45afbededc1bf71a9c3fcf13288ff131af779 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sat, 27 Sep 2025 11:43:19 -0400 Subject: [PATCH 1/4] Add ability to resize Code panel --- src/App.tsx | 102 ++++++++++++++++--- src/i18n/locales/en/translation.json | 2 + src/i18n/locales/es/translation.json | 2 + src/i18n/locales/he/translation.json | 2 + src/reactComponents/CodeDisplay.tsx | 58 ++++++++++- src/reactComponents/SiderCollapseTrigger.tsx | 74 ++++++++++++++ 6 files changed, 221 insertions(+), 19 deletions(-) create mode 100644 src/reactComponents/SiderCollapseTrigger.tsx diff --git a/src/App.tsx b/src/App.tsx index a449a07d..14c14979 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,7 @@ import { pythonGenerator } from 'blockly/python'; import Header from './reactComponents/Header'; import * as Menu from './reactComponents/Menu'; import CodeDisplay from './reactComponents/CodeDisplay'; +import SiderCollapseTrigger from './reactComponents/SiderCollapseTrigger'; import BlocklyComponent, { BlocklyComponentType } from './reactComponents/BlocklyComponent'; import ToolboxSettingsModal from './reactComponents/ToolboxSettings'; import * as Tabs from './reactComponents/Tabs'; @@ -78,10 +79,10 @@ const FULL_VIEWPORT_HEIGHT = '100vh'; const FULL_HEIGHT = '100%'; /** Default size for code panel. */ -const CODE_PANEL_DEFAULT_SIZE = '25%'; +const CODE_PANEL_DEFAULT_SIZE = 400; /** Minimum size for code panel. */ -const CODE_PANEL_MIN_SIZE = 80; +const CODE_PANEL_MIN_SIZE = 100; /** Background color for testing layout. */ const LAYOUT_BACKGROUND_COLOR = '#0F0'; @@ -166,7 +167,10 @@ const AppContent: React.FC = ({ project, setProject }): React.J const [shownPythonToolboxCategories, setShownPythonToolboxCategories] = React.useState>(new Set()); const [triggerPythonRegeneration, setTriggerPythonRegeneration] = React.useState(0); const [leftCollapsed, setLeftCollapsed] = React.useState(false); - const [rightCollapsed, setRightCollapsed] = React.useState(false); + const [codePanelSize, setCodePanelSize] = React.useState(CODE_PANEL_DEFAULT_SIZE); + const [codePanelCollapsed, setCodePanelCollapsed] = React.useState(false); + const [codePanelExpandedSize, setCodePanelExpandedSize] = React.useState(CODE_PANEL_DEFAULT_SIZE); + const [codePanelAnimating, setCodePanelAnimating] = React.useState(false); const [theme, setTheme] = React.useState('dark'); const [languageInitialized, setLanguageInitialized] = React.useState(false); const [themeInitialized, setThemeInitialized] = React.useState(false); @@ -378,6 +382,27 @@ const AppContent: React.FC = ({ project, setProject }): React.J setToolboxSettingsModalIsOpen(false); }; + /** Toggles the code panel between collapsed and expanded states. */ + const toggleCodePanelCollapse = (): void => { + setCodePanelAnimating(true); + + if (codePanelCollapsed) { + // Expand to previous size + setCodePanelSize(codePanelExpandedSize); + setCodePanelCollapsed(false); + } else { + // Collapse to minimum size + setCodePanelExpandedSize(codePanelSize); + setCodePanelSize(CODE_PANEL_MIN_SIZE); + setCodePanelCollapsed(true); + } + + // Reset animation flag after transition completes + setTimeout(() => { + setCodePanelAnimating(false); + }, 200); + }; + /** Handles toolbox settings modal OK with updated categories. */ const handleToolboxSettingsConfirm = (updatedShownCategories: Set): void => { setToolboxSettingsModalIsOpen(false); @@ -618,6 +643,8 @@ const AppContent: React.FC = ({ project, setProject }): React.J collapsible collapsed={leftCollapsed} onCollapse={(collapsed: boolean) => setLeftCollapsed(collapsed)} + trigger={null} + style={{ position: 'relative' }} > = ({ project, setProject }): React.J theme={theme} setTheme={setTheme} /> + setLeftCollapsed(!leftCollapsed)} + /> = ({ project, setProject }): React.J setProject={setProject} storage={storage} /> - - +
+ {modulePaths.current.map((modulePath) => ( = ({ project, setProject }): React.J /> ))} - setRightCollapsed(collapsed)} +
+
{ + e.preventDefault(); + const startX = e.clientX; + const startWidth = codePanelSize; + + const handleMouseMove = (e: MouseEvent) => { + const deltaX = startX - e.clientX; + const newWidth = Math.max(CODE_PANEL_MIN_SIZE, startWidth + deltaX); + setCodePanelSize(newWidth); + // Update expanded size if not at minimum + if (newWidth > CODE_PANEL_MIN_SIZE) { + setCodePanelExpandedSize(newWidth); + setCodePanelCollapsed(false); + } else { + setCodePanelCollapsed(true); + } + }; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }} + /> - - +
+
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index f2561b70..4c37d0cd 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -43,6 +43,8 @@ "BLOCKS": "Blocks", "CODE": "Code", "COPY": "Copy", + "COLLAPSE": "Collapse", + "EXPAND": "Expand", "FAILED_TO_RENAME_PROJECT": "Failed to rename project", "FAILED_TO_COPY_PROJECT": "Failed to copy project", "FAILED_TO_CREATE_PROJECT": "Failed to create a new project.", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index b7c84a1e..73b03b33 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -40,6 +40,8 @@ "BLOCKS": "Bloques", "CODE": "Código", "COPY": "Copiar", + "COLLAPSE": "Colapsar", + "EXPAND": "Expandir", "FAILED_TO_RENAME_PROJECT": "Error al renombrar proyecto", "FAILED_TO_COPY_PROJECT": "Error al copiar proyecto", "FAILED_TO_CREATE_PROJECT": "Error al crear un nuevo proyecto.", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index f748ff3a..86238a3c 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -43,6 +43,8 @@ "BLOCKS": "בלוקים", "CODE": "קוד", "COPY": "העתק", + "COLLAPSE": "כווץ", + "EXPAND": "הרחב", "FAILED_TO_RENAME_PROJECT": "נכשל בשינוי שם הפרויקט", "FAILED_TO_COPY_PROJECT": "נכשל בהעתקת הפרויקט", "FAILED_TO_CREATE_PROJECT": "נכשל ביצירת פרויקט חדש.", diff --git a/src/reactComponents/CodeDisplay.tsx b/src/reactComponents/CodeDisplay.tsx index 88c03170..a7b39c93 100644 --- a/src/reactComponents/CodeDisplay.tsx +++ b/src/reactComponents/CodeDisplay.tsx @@ -20,7 +20,7 @@ */ import * as Antd from 'antd'; import * as React from 'react'; -import { CopyOutlined as CopyIcon } from '@ant-design/icons'; +import { CopyOutlined as CopyIcon, LeftOutlined as CollapseIcon, RightOutlined as ExpandIcon } from '@ant-design/icons'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { dracula, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; @@ -36,6 +36,8 @@ interface CodeDisplayProps { theme: string; messageApi: MessageInstance; setAlertErrorMessage: StringFunction; + isCollapsed?: boolean; + onToggleCollapse?: () => void; } /** Success message for copy operation. */ @@ -110,10 +112,56 @@ export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element ); + /** Renders the collapse/expand trigger at the bottom center of the panel. */ + const renderCollapseTrigger = (): React.JSX.Element | null => { + if (!props.onToggleCollapse) return null; + + return ( +
{ + e.currentTarget.style.color = token.colorText; + e.currentTarget.style.backgroundColor = token.colorBgTextHover; + }} + onMouseLeave={(e) => { + e.currentTarget.style.color = token.colorTextSecondary; + e.currentTarget.style.backgroundColor = token.colorBgContainer; + }} + > + + {props.isCollapsed ? : } + +
+ ); + }; + return ( - - {renderHeader()} - {renderCodeBlock()} - +
+ + {renderHeader()} + {renderCodeBlock()} + + {renderCollapseTrigger()} +
); } diff --git a/src/reactComponents/SiderCollapseTrigger.tsx b/src/reactComponents/SiderCollapseTrigger.tsx new file mode 100644 index 00000000..44563e0f --- /dev/null +++ b/src/reactComponents/SiderCollapseTrigger.tsx @@ -0,0 +1,74 @@ +/** + * @license + * Copyright 2025 Porpoiseful LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author alan@porpoiseful.com (Alan Smith) + */ +import * as React from 'react'; +import * as Antd from 'antd'; +import { LeftOutlined, RightOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; + +/** Props for the SiderCollapseTrigger component. */ +interface SiderCollapseTriggerProps { + collapsed: boolean; + onToggle: () => void; +} + +/** + * Custom collapse trigger for Sider that matches the right panel's appearance. + */ +export default function SiderCollapseTrigger(props: SiderCollapseTriggerProps): React.JSX.Element { + const { token } = Antd.theme.useToken(); + const { t } = useTranslation(); + const [isHovered, setIsHovered] = React.useState(false); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {props.collapsed ? + : + + } + +
+ ); +} \ No newline at end of file From ce488c6bab3ceb766a4ed768a47b6791f6220d0a Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sat, 27 Sep 2025 11:47:08 -0400 Subject: [PATCH 2/4] Change to use the new collapse committment in the code display --- src/reactComponents/CodeDisplay.tsx | 42 +++++------------------------ 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/src/reactComponents/CodeDisplay.tsx b/src/reactComponents/CodeDisplay.tsx index a7b39c93..9ea0ffdb 100644 --- a/src/reactComponents/CodeDisplay.tsx +++ b/src/reactComponents/CodeDisplay.tsx @@ -20,8 +20,9 @@ */ import * as Antd from 'antd'; import * as React from 'react'; -import { CopyOutlined as CopyIcon, LeftOutlined as CollapseIcon, RightOutlined as ExpandIcon } from '@ant-design/icons'; +import { CopyOutlined as CopyIcon } from '@ant-design/icons'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import SiderCollapseTrigger from './SiderCollapseTrigger'; import { dracula, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; import type { MessageInstance } from 'antd/es/message/interface'; @@ -117,41 +118,10 @@ export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element if (!props.onToggleCollapse) return null; return ( -
{ - e.currentTarget.style.color = token.colorText; - e.currentTarget.style.backgroundColor = token.colorBgTextHover; - }} - onMouseLeave={(e) => { - e.currentTarget.style.color = token.colorTextSecondary; - e.currentTarget.style.backgroundColor = token.colorBgContainer; - }} - > - - {props.isCollapsed ? : } - -
+ ); }; From f502b234f47288d0ab57f2955c2f64eec9cb4d03 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sat, 27 Sep 2025 11:50:26 -0400 Subject: [PATCH 3/4] Change to make default starting size a percentage --- src/App.tsx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 14c14979..09f05d8b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -79,7 +79,7 @@ const FULL_VIEWPORT_HEIGHT = '100vh'; const FULL_HEIGHT = '100%'; /** Default size for code panel. */ -const CODE_PANEL_DEFAULT_SIZE = 400; +const CODE_PANEL_DEFAULT_SIZE = '25%'; /** Minimum size for code panel. */ const CODE_PANEL_MIN_SIZE = 100; @@ -167,9 +167,9 @@ const AppContent: React.FC = ({ project, setProject }): React.J const [shownPythonToolboxCategories, setShownPythonToolboxCategories] = React.useState>(new Set()); const [triggerPythonRegeneration, setTriggerPythonRegeneration] = React.useState(0); const [leftCollapsed, setLeftCollapsed] = React.useState(false); - const [codePanelSize, setCodePanelSize] = React.useState(CODE_PANEL_DEFAULT_SIZE); + const [codePanelSize, setCodePanelSize] = React.useState(CODE_PANEL_DEFAULT_SIZE); const [codePanelCollapsed, setCodePanelCollapsed] = React.useState(false); - const [codePanelExpandedSize, setCodePanelExpandedSize] = React.useState(CODE_PANEL_DEFAULT_SIZE); + const [codePanelExpandedSize, setCodePanelExpandedSize] = React.useState(CODE_PANEL_DEFAULT_SIZE); const [codePanelAnimating, setCodePanelAnimating] = React.useState(false); const [theme, setTheme] = React.useState('dark'); const [languageInitialized, setLanguageInitialized] = React.useState(false); @@ -391,8 +391,11 @@ const AppContent: React.FC = ({ project, setProject }): React.J setCodePanelSize(codePanelExpandedSize); setCodePanelCollapsed(false); } else { - // Collapse to minimum size - setCodePanelExpandedSize(codePanelSize); + // Collapse to minimum size - convert current size to pixels for storage + const currentSizePx = typeof codePanelSize === 'string' + ? (parseFloat(codePanelSize) / 100) * window.innerWidth + : codePanelSize; + setCodePanelExpandedSize(currentSizePx); setCodePanelSize(CODE_PANEL_MIN_SIZE); setCodePanelCollapsed(true); } @@ -687,7 +690,7 @@ const AppContent: React.FC = ({ project, setProject }): React.J
= ({ project, setProject }): React.J const handleMouseMove = (e: MouseEvent) => { const deltaX = startX - e.clientX; - const newWidth = Math.max(CODE_PANEL_MIN_SIZE, startWidth + deltaX); + // Convert startWidth to number if it's a percentage + const startWidthPx = typeof startWidth === 'string' + ? (parseFloat(startWidth) / 100) * window.innerWidth + : startWidth; + const newWidth = Math.max(CODE_PANEL_MIN_SIZE, startWidthPx + deltaX); setCodePanelSize(newWidth); // Update expanded size if not at minimum if (newWidth > CODE_PANEL_MIN_SIZE) { From d7a427aa0fc4f508df6b21b745b5fe19a42050dd Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sun, 28 Sep 2025 08:56:42 -0400 Subject: [PATCH 4/4] Reverse arrow on right panel so it makes logical sense --- src/reactComponents/CodeDisplay.tsx | 1 + src/reactComponents/SiderCollapseTrigger.tsx | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/reactComponents/CodeDisplay.tsx b/src/reactComponents/CodeDisplay.tsx index 9ea0ffdb..b5625700 100644 --- a/src/reactComponents/CodeDisplay.tsx +++ b/src/reactComponents/CodeDisplay.tsx @@ -121,6 +121,7 @@ export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element ); }; diff --git a/src/reactComponents/SiderCollapseTrigger.tsx b/src/reactComponents/SiderCollapseTrigger.tsx index 44563e0f..7808fda8 100644 --- a/src/reactComponents/SiderCollapseTrigger.tsx +++ b/src/reactComponents/SiderCollapseTrigger.tsx @@ -27,6 +27,7 @@ import { useTranslation } from 'react-i18next'; interface SiderCollapseTriggerProps { collapsed: boolean; onToggle: () => void; + isRightPanel?: boolean; } /** @@ -64,10 +65,17 @@ export default function SiderCollapseTrigger(props: SiderCollapseTriggerProps): onMouseLeave={() => setIsHovered(false)} > - {props.collapsed ? - : - - } + {props.isRightPanel ? ( + // Right panel: reversed arrows + props.collapsed ? + : + + ) : ( + // Left panel: normal arrows + props.collapsed ? + : + + )}
);