Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 94 additions & 13 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -166,7 +167,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
const [shownPythonToolboxCategories, setShownPythonToolboxCategories] = React.useState<Set<string>>(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<string | number>(CODE_PANEL_DEFAULT_SIZE);
const [codePanelCollapsed, setCodePanelCollapsed] = React.useState(false);
const [codePanelExpandedSize, setCodePanelExpandedSize] = React.useState<string | number>(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);
Expand Down Expand Up @@ -378,6 +382,30 @@ const AppContent: React.FC<AppContentProps> = ({ 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<string>): void => {
setToolboxSettingsModalIsOpen(false);
Expand Down Expand Up @@ -618,6 +646,8 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
collapsible
collapsed={leftCollapsed}
onCollapse={(collapsed: boolean) => setLeftCollapsed(collapsed)}
trigger={null}
style={{ position: 'relative' }}
>
<Menu.Component
storage={storage}
Expand All @@ -629,6 +659,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
theme={theme}
setTheme={setTheme}
/>
<SiderCollapseTrigger
collapsed={leftCollapsed}
onToggle={() => setLeftCollapsed(!leftCollapsed)}
/>
</Sider>
<Antd.Layout>
<Tabs.Component
Expand All @@ -642,8 +676,8 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
setProject={setProject}
storage={storage}
/>
<Antd.Layout>
<Content>
<div style={{ display: 'flex', height: FULL_HEIGHT }}>
<Content style={{ flex: 1, height: '100%' }}>
{modulePaths.current.map((modulePath) => (
<BlocklyComponent
key={modulePath}
Expand All @@ -654,22 +688,69 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
/>
))}
</Content>
<Sider
collapsible
reverseArrow={true}
collapsed={rightCollapsed}
collapsedWidth={CODE_PANEL_MIN_SIZE}
width={CODE_PANEL_DEFAULT_SIZE}
onCollapse={(collapsed: boolean) => setRightCollapsed(collapsed)}
<div
style={{
width: typeof codePanelSize === 'string' ? codePanelSize : `${codePanelSize}px`,
minWidth: CODE_PANEL_MIN_SIZE,
height: '100%',
borderLeft: '1px solid #d9d9d9',
position: 'relative',
transition: codePanelAnimating ? 'width 0.2s ease' : 'none'
}}
>
<div
style={{
position: 'absolute',
left: 0,
top: 0,
width: '4px',
height: '100%',
cursor: 'ew-resize',
backgroundColor: 'transparent',
zIndex: 10,
transform: 'translateX(-2px)'
}}
onMouseDown={(e) => {
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);
}}
/>
<CodeDisplay
generatedCode={generatedCode}
messageApi={messageApi}
setAlertErrorMessage={setAlertErrorMessage}
theme={theme}
isCollapsed={codePanelCollapsed}
onToggleCollapse={toggleCodePanelCollapse}
/>
</Sider>
</Antd.Layout>
</div>
</div>
</Antd.Layout>
</Antd.Layout>
</Antd.Layout>
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/he/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
"BLOCKS": "בלוקים",
"CODE": "קוד",
"COPY": "העתק",
"COLLAPSE": "כווץ",
"EXPAND": "הרחב",
"FAILED_TO_RENAME_PROJECT": "נכשל בשינוי שם הפרויקט",
"FAILED_TO_COPY_PROJECT": "נכשל בהעתקת הפרויקט",
"FAILED_TO_CREATE_PROJECT": "נכשל ביצירת פרויקט חדש.",
Expand Down
27 changes: 23 additions & 4 deletions src/reactComponents/CodeDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -36,6 +37,8 @@ interface CodeDisplayProps {
theme: string;
messageApi: MessageInstance;
setAlertErrorMessage: StringFunction;
isCollapsed?: boolean;
onToggleCollapse?: () => void;
}

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

/** Renders the collapse/expand trigger at the bottom center of the panel. */
const renderCollapseTrigger = (): React.JSX.Element | null => {
if (!props.onToggleCollapse) return null;

return (
<SiderCollapseTrigger
collapsed={props.isCollapsed || false}
onToggle={props.onToggleCollapse}
isRightPanel={true}
/>
);
};

return (
<Antd.Flex vertical gap="small" style={{ height: '100%' }}>
{renderHeader()}
{renderCodeBlock()}
</Antd.Flex>
<div style={{ height: '100%', position: 'relative' }}>
<Antd.Flex vertical gap="small" style={{ height: '100%' }}>
{renderHeader()}
{renderCodeBlock()}
</Antd.Flex>
{renderCollapseTrigger()}
</div>
);
}
82 changes: 82 additions & 0 deletions src/reactComponents/SiderCollapseTrigger.tsx
Original file line number Diff line number Diff line change
@@ -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 [email protected] (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 (
<div
style={{
position: 'absolute',
bottom: 0,
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: isHovered ? token.colorBgTextHover : token.colorBgContainer,
border: `1px solid ${token.colorBorder}`,
borderBottom: 'none',
borderRadius: '6px 6px 0 0',
padding: '2px 6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: '24px',
height: '22px',
color: isHovered ? token.colorText : token.colorTextSecondary,
transition: 'all 0.2s',
zIndex: 1,
}}
onClick={props.onToggle}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Antd.Tooltip title={props.collapsed ? t("EXPAND") : t("COLLAPSE")}>
{props.isRightPanel ? (
// Right panel: reversed arrows
props.collapsed ?
<LeftOutlined style={{ fontSize: '12px', color: 'inherit' }} /> :
<RightOutlined style={{ fontSize: '12px', color: 'inherit' }} />
) : (
// Left panel: normal arrows
props.collapsed ?
<RightOutlined style={{ fontSize: '12px', color: 'inherit' }} /> :
<LeftOutlined style={{ fontSize: '12px', color: 'inherit' }} />
)}
</Antd.Tooltip>
</div>
);
}