diff --git a/src/App.tsx b/src/App.tsx index a449a07d..09f05d8b 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'; @@ -81,7 +82,7 @@ const FULL_HEIGHT = '100%'; const CODE_PANEL_DEFAULT_SIZE = '25%'; /** 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,30 @@ 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 - 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); + } + + // 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 +646,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; + // 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) { + 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..b5625700 100644 --- a/src/reactComponents/CodeDisplay.tsx +++ b/src/reactComponents/CodeDisplay.tsx @@ -22,6 +22,7 @@ import * as Antd from 'antd'; import * as React from 'react'; 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'; @@ -36,6 +37,8 @@ interface CodeDisplayProps { theme: string; messageApi: MessageInstance; setAlertErrorMessage: StringFunction; + isCollapsed?: boolean; + onToggleCollapse?: () => void; } /** Success message for copy operation. */ @@ -110,10 +113,26 @@ 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 ( + + ); + }; + return ( - - {renderHeader()} - {renderCodeBlock()} - +
+ + {renderHeader()} + {renderCodeBlock()} + + {renderCollapseTrigger()} +
); } diff --git a/src/reactComponents/SiderCollapseTrigger.tsx b/src/reactComponents/SiderCollapseTrigger.tsx new file mode 100644 index 00000000..7808fda8 --- /dev/null +++ b/src/reactComponents/SiderCollapseTrigger.tsx @@ -0,0 +1,82 @@ +/** + * @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; + isRightPanel?: boolean; +} + +/** + * 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.isRightPanel ? ( + // Right panel: reversed arrows + props.collapsed ? + : + + ) : ( + // Left panel: normal arrows + props.collapsed ? + : + + )} + +
+ ); +} \ No newline at end of file