diff --git a/src/components/app-wrapper.jsx b/src/components/app-wrapper.jsx index 16fb774ba3..7fe47ef274 100644 --- a/src/components/app-wrapper.jsx +++ b/src/components/app-wrapper.jsx @@ -208,8 +208,8 @@ const lightTheme = createTheme({ reactflow: { backgroundColor: 'white', labeledGroup: { - backgroundColor: 'white', - borderColor: '#11161A', + backgroundColor: '#FAFAFA', + borderColor: '#BDBDBD', }, edge: { stroke: '#6F767B', diff --git a/src/components/graph/layout.ts b/src/components/graph/layout.ts index 3d1d22e4ca..f00ddd3baa 100644 --- a/src/components/graph/layout.ts +++ b/src/components/graph/layout.ts @@ -10,8 +10,8 @@ import { NODE_HEIGHT, NODE_WIDTH } from './nodes/constants'; import { groupIdSuffix, LABELED_GROUP_TYPE } from './nodes/labeled-group-node.type'; import { CurrentTreeNode, isSecurityModificationNode, NetworkModificationNodeType } from './tree-node.type'; -const widthSpacing = 70; -const heightSpacing = 90; +const widthSpacing = 110; +const heightSpacing = 140; export const nodeWidth = NODE_WIDTH + widthSpacing; export const nodeHeight = NODE_HEIGHT + heightSpacing; export const snapGrid = [10, nodeHeight]; // Used for drag and drop diff --git a/src/components/graph/nodes/build-button.tsx b/src/components/graph/nodes/build-button.tsx index af404981c2..51c50b7b4c 100644 --- a/src/components/graph/nodes/build-button.tsx +++ b/src/components/graph/nodes/build-button.tsx @@ -94,7 +94,9 @@ export const BuildButton = ({ return !buildStatus || buildStatus === BUILD_STATUS.NOT_BUILT ? ( ) : ( - + (theme.palette.mode === 'light' ? theme.palette.primary.light : undefined) }} + /> ); }; diff --git a/src/components/graph/nodes/build-status-chip.styles.ts b/src/components/graph/nodes/build-status-chip.styles.ts new file mode 100644 index 0000000000..4710029a6c --- /dev/null +++ b/src/components/graph/nodes/build-status-chip.styles.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { type SxStyle } from '@gridsuite/commons-ui'; +import { Theme } from '@mui/material'; + +export const buildStatusChipStyles = { + base: (theme: Theme) => + ({ + padding: theme.spacing(1, 0.5), + fontSize: '12px', + fontWeight: 400, + lineHeight: '100%', + }) as const, + + notBuilt: { + background: (theme: Theme) => theme.node.buildStatus.notBuilt, + color: (theme: Theme) => theme.palette.getContrastText(theme.node.buildStatus.notBuilt), + } as SxStyle, +} as const; diff --git a/src/components/graph/nodes/build-status-chip.tsx b/src/components/graph/nodes/build-status-chip.tsx index ed2beca07b..f34384fd73 100644 --- a/src/components/graph/nodes/build-status-chip.tsx +++ b/src/components/graph/nodes/build-status-chip.tsx @@ -6,53 +6,26 @@ */ import React, { ReactElement } from 'react'; -import { Chip } from '@mui/material'; +import { Chip, useTheme } from '@mui/material'; import { useIntl } from 'react-intl'; import { BUILD_STATUS } from 'components/network/constants'; import { mergeSx, type SxStyle } from '@gridsuite/commons-ui'; - -function getBuildStatusSx(buildStatus: BUILD_STATUS | undefined): SxStyle { - return (theme) => { - const bs = theme.node.buildStatus; - // pick background based on status - let bg: string; - - switch (buildStatus) { - case BUILD_STATUS.BUILT: - bg = bs.success; - break; - case BUILD_STATUS.BUILT_WITH_WARNING: - bg = bs.warning; - break; - case BUILD_STATUS.BUILT_WITH_ERROR: - bg = bs.error; - break; - default: - bg = bs.notBuilt; - break; - } - - // only set explicit contrast color when it's the "notBuilt" background - const shouldSetContrast = bg === bs.notBuilt; - - return { - background: bg, - ...(shouldSetContrast ? { color: theme.palette.getContrastText(bg) } : {}), - '&:hover': { - backgroundColor: bg, - }, - }; - }; +import { zoomStyles } from '../zoom.styles'; +import { buildStatusChipStyles } from './build-status-chip.styles'; + +function getBuildStatusColor(buildStatus: BUILD_STATUS | undefined) { + switch (buildStatus) { + case BUILD_STATUS.BUILT: + return 'success'; + case BUILD_STATUS.BUILT_WITH_WARNING: + return 'warning'; + case BUILD_STATUS.BUILT_WITH_ERROR: + return 'error'; + default: + return undefined; + } } -const baseStyle: SxStyle = (theme) => - ({ - padding: theme.spacing(1, 0.5), - fontSize: '12px', - fontWeight: 400, - lineHeight: '100%', - }) as const; - type BuildStatusChipProps = { buildStatus?: BUILD_STATUS; sx?: SxStyle; @@ -62,18 +35,24 @@ type BuildStatusChipProps = { const BuildStatusChip = ({ buildStatus = BUILD_STATUS.NOT_BUILT, sx, icon, onClick }: BuildStatusChipProps) => { const intl = useIntl(); + const theme = useTheme(); + + const showLabel = zoomStyles.visibility.showBuildStatusLabel(theme); + const label = showLabel ? intl.formatMessage({ id: buildStatus }) : undefined; + const color = getBuildStatusColor(buildStatus); - const label = intl.formatMessage({ id: buildStatus }); + // Custom styling for NOT_BUILT status (no standard MUI color) + const notBuiltSx: SxStyle | undefined = color === undefined ? buildStatusChipStyles.notBuilt : undefined; - return ( - + // Combine styles: base + compact circular (if no label) + notBuilt color + parent overrides + const finalSx = mergeSx( + buildStatusChipStyles.base, + showLabel ? undefined : (theme) => zoomStyles.layout.getCompactChipSize(theme), + notBuiltSx, + sx ); + + return ; }; export default BuildStatusChip; diff --git a/src/components/graph/nodes/labeled-group-node.styles.ts b/src/components/graph/nodes/labeled-group-node.styles.ts new file mode 100644 index 0000000000..de3b481391 --- /dev/null +++ b/src/components/graph/nodes/labeled-group-node.styles.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { type MuiStyles } from '@gridsuite/commons-ui'; +import { colors, Theme } from '@mui/material'; +import { zoomStyles } from '../zoom.styles'; +import { DETAIL_LEVELS } from '../zoom.constants'; + +export const LABEL_GROUP_OFFSET = 30; + +export const getContainerStyle = (theme: Theme, isLight: boolean) => { + let backgroundColor = 'transparent'; + if (theme.tree?.detailLevel === DETAIL_LEVELS.MINIMAL) { + backgroundColor = isLight ? theme.palette.grey[200] : colors.grey[700]; + } + + return { + ...zoomStyles.labeledGroupBorder(theme), + background: backgroundColor, + borderColor: isLight ? colors.grey[400] : colors.grey[500], + }; +}; + +export const labeledGroupNodeStyles = { + label: (theme) => ({ + position: 'absolute', + top: -15, + right: 20, + backgroundColor: theme.reactflow.labeledGroup.backgroundColor, + padding: '3px 6px', + border: '1px solid', + borderColor: theme.reactflow.labeledGroup.borderColor, + boxShadow: theme.shadows[1], + fontSize: '13px', + display: theme.tree?.detailLevel === DETAIL_LEVELS.MINIMAL ? 'none' : 'flex', + gap: '4px', + alignItems: 'center', + justifyContent: 'center', + lineHeight: 1, + }), + + icon: (theme) => ({ + fontSize: zoomStyles.iconSize(theme), + verticalAlign: 'middle', + }), + + text: (theme) => ({ + display: theme.tree?.detailLevel === DETAIL_LEVELS.STANDARD ? 'inline' : 'none', + verticalAlign: 'middle', + }), +} as const satisfies MuiStyles; diff --git a/src/components/graph/nodes/labeled-group-node.tsx b/src/components/graph/nodes/labeled-group-node.tsx index b18d07ca2a..03f02f12a3 100644 --- a/src/components/graph/nodes/labeled-group-node.tsx +++ b/src/components/graph/nodes/labeled-group-node.tsx @@ -5,50 +5,34 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Box } from '@mui/material'; -import { NodeProps, ReactFlowState, useStore } from '@xyflow/react'; -import { nodeHeight as nodeLayoutHeight, nodeWidth as nodeLayoutWidth } from '../layout'; +import { Box, useTheme } from '@mui/material'; +import { NodeProps } from '@xyflow/react'; import SecurityIcon from '@mui/icons-material/Security'; +import { nodeHeight as nodeLayoutHeight, nodeWidth as nodeLayoutWidth } from '../layout'; import { FormattedMessage } from 'react-intl'; -import { type MuiStyles } from '@gridsuite/commons-ui'; import { LabeledGroupNodeType } from './labeled-group-node.type'; import { NODE_HEIGHT, NODE_WIDTH } from './constants'; - -const styles = { - border: { - border: 'dashed 3px #8B8F8F', - borderRadius: '8px', - }, - label: (theme) => ({ - position: 'absolute', - top: -13, - right: 8, - backgroundColor: theme.reactflow.labeledGroup.backgroundColor, - padding: '0px 6px', - border: '1px solid', - borderColor: theme.reactflow.labeledGroup.borderColor, - fontSize: 12, - display: 'flex', - gap: '10px', - alignItems: 'center', - }), -} as const satisfies MuiStyles; +import { labeledGroupNodeStyles, getContainerStyle, LABEL_GROUP_OFFSET } from './labeled-group-node.styles'; export function LabeledGroupNode({ data }: NodeProps) { + const theme = useTheme(); // Vertically, the border is halfway between the node and the edge above, // and since that edge is centered between two nodes, we divide the space by 4. const verticalPadding = (nodeLayoutHeight - NODE_HEIGHT) / 4; // horizontally, the border is placed exactly halfway between two nodes — that's why we divide the space between them by 2. const horizontalPadding = (nodeLayoutWidth - NODE_WIDTH) / 2; - const labeledGroupTopPosition = data.position.topLeft.row * nodeLayoutHeight - verticalPadding; + // Adjust position and size to account for border width and label block + const labeledGroupTopPosition = data.position.topLeft.row * nodeLayoutHeight - verticalPadding - LABEL_GROUP_OFFSET; const labeledGroupLeftPosition = data.position.topLeft.column * nodeLayoutWidth - horizontalPadding; const labeledGroupHeight = - (data.position.bottomRight.row - data.position.topLeft.row + 1) * nodeLayoutHeight - 2 * verticalPadding; + (data.position.bottomRight.row - data.position.topLeft.row + 1) * nodeLayoutHeight - + 2 * verticalPadding + + 2 * LABEL_GROUP_OFFSET; const labeledGroupWidth = (data.position.bottomRight.column - data.position.topLeft.column + 1) * nodeLayoutWidth; - const zoom = useStore((s: ReactFlowState) => s.transform?.[2] ?? 1); + const isLight = theme.palette.mode === 'light'; return ( ) { left={labeledGroupLeftPosition} height={labeledGroupHeight} width={labeledGroupWidth} - sx={styles.border} + sx={getContainerStyle(theme, isLight)} > - {zoom >= 0.5 && ( - - + + + - )} + ); } diff --git a/src/components/graph/nodes/network-modification-node.styles.ts b/src/components/graph/nodes/network-modification-node.styles.ts new file mode 100644 index 0000000000..1c5c391c3d --- /dev/null +++ b/src/components/graph/nodes/network-modification-node.styles.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { type MuiStyles } from '@gridsuite/commons-ui'; +import { Theme } from '@mui/material'; +import { baseNodeStyles, interactiveNodeStyles } from './styles'; +import { zoomStyles } from '../zoom.styles'; + +export const getBorderWidthStyle = (theme: Theme, isSelected: boolean) => ({ + borderWidth: zoomStyles.borderWidth(theme, isSelected), +}); + +export const getNodeBaseStyle = (theme: Theme, isSelected: boolean) => + isSelected ? modificationNodeStyles.selected(theme) : modificationNodeStyles.default(theme); + +export const modificationNodeStyles = { + selected: (theme) => ({ + ...baseNodeStyles(theme, 'column'), + background: theme.node.modification.selectedBackground, + border: theme.node.modification.selectedBorder, + boxShadow: theme.shadows[6], + ...interactiveNodeStyles(theme, 'modification'), + }), + + default: (theme) => ({ + ...baseNodeStyles(theme, 'column'), + border: theme.node.modification.border, + ...interactiveNodeStyles(theme, 'modification'), + }), + + contentBox: (theme) => ({ + flexGrow: 1, + display: zoomStyles.visibility.showNodeContent(theme) ? 'flex' : 'none', + alignItems: 'flex-end', + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + marginBottom: theme.spacing(1), + }), + + typographyText: (theme) => ({ + color: theme.palette.text.primary, + fontSize: '20px', + fontWeight: 400, + lineHeight: 'normal', + textAlign: 'left', + display: '-webkit-box', + WebkitBoxOrient: 'vertical', + WebkitLineClamp: 2, + overflow: 'hidden', + width: 'auto', + textOverflow: 'ellipsis', + wordBreak: 'break-word', + }), + + footer: (theme) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + height: zoomStyles.layout.useFullHeightFooter(theme) ? '100%' : '35%', + }), + + chipFloating: (theme) => ({ + position: 'absolute', + top: theme.spacing(-4.3), + left: theme.spacing(1), + zIndex: 2, + }), + + chipLarge: (theme) => zoomStyles.layout.getLargeChipSize(theme) ?? {}, + + buildButtonContainer: (theme) => ({ + display: zoomStyles.visibility.showBuildButton(theme) ? 'block' : 'none', + }), + + globalBuildStatusIcon: (theme) => ({ + fontSize: zoomStyles.iconSize(theme), + '& path': { + stroke: `currentColor`, + strokeWidth: zoomStyles.iconStrokeWidth(theme), + }, + }), + + tooltip: { + maxWidth: '720px', + }, +} as const satisfies MuiStyles; diff --git a/src/components/graph/nodes/network-modification-node.tsx b/src/components/graph/nodes/network-modification-node.tsx index 7bec54d513..d047e75b02 100644 --- a/src/components/graph/nodes/network-modification-node.tsx +++ b/src/components/graph/nodes/network-modification-node.tsx @@ -9,82 +9,24 @@ import { NodeProps, Position } from '@xyflow/react'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import { useSelector } from 'react-redux'; import Box from '@mui/material/Box'; -import { LIGHT_THEME, type MuiStyles } from '@gridsuite/commons-ui'; +import { LIGHT_THEME } from '@gridsuite/commons-ui'; import { getLocalStorageTheme } from '../../../redux/session-storage/local-storage'; import { BUILD_STATUS } from '../../network/constants'; import { AppState } from 'redux/reducer'; import { CopyType } from 'components/network-modification.type'; import { ModificationNode } from '../tree-node.type'; import NodeHandle from './node-handle'; -import { baseNodeStyles, interactiveNodeStyles } from './styles'; import NodeOverlaySpinner from './node-overlay-spinner'; import BuildStatusChip from './build-status-chip'; - +import React, { useCallback, useMemo } from 'react'; import { BuildButton } from './build-button'; -import { Tooltip, Typography } from '@mui/material'; +import { Tooltip, Typography, useTheme } from '@mui/material'; import { useIntl } from 'react-intl'; -import { useMemo } from 'react'; import { TOOLTIP_DELAY } from 'utils/UIconstants'; import ForwardRefBox from 'components/utils/forwardRefBox'; - -const styles = { - networkModificationSelected: (theme) => ({ - ...baseNodeStyles(theme, 'column'), - background: theme.node.modification.selectedBackground, - border: theme.node.modification.selectedBorder, - boxShadow: theme.shadows[6], - ...interactiveNodeStyles(theme, 'modification'), - }), - networkModification: (theme) => ({ - ...baseNodeStyles(theme, 'column'), - border: theme.node.modification.border, - ...interactiveNodeStyles(theme, 'modification'), - }), - contentBox: (theme) => ({ - flexGrow: 1, - display: 'flex', - alignItems: 'flex-end', - marginLeft: theme.spacing(1), - marginRight: theme.spacing(1), - marginBottom: theme.spacing(1), - }), - typographyText: (theme) => ({ - color: theme.palette.text.primary, - fontSize: '20px', - fontWeight: 400, - lineHeight: 'normal', - textAlign: 'left', - display: '-webkit-box', - WebkitBoxOrient: 'vertical', - WebkitLineClamp: 2, - overflow: 'hidden', - width: 'auto', - textOverflow: 'ellipsis', - wordBreak: 'break-word', - }), - footerBox: (theme) => ({ - display: 'flex', - justifyContent: 'flex-start', - marginLeft: theme.spacing(1), - height: '35%', - }), - buildBox: (theme) => ({ - display: 'flex', - justifyContent: 'flex-end', - marginTop: theme.spacing(-5), - marginRight: theme.spacing(0), - height: '35%', - }), - chipFloating: (theme) => ({ - position: 'absolute', - top: theme.spacing(-4), - left: theme.spacing(1), - zIndex: 2, - }), - tooltip: { - maxWidth: '720px', - }, -} as const satisfies MuiStyles; +import { zoomStyles } from '../zoom.styles'; +import { modificationNodeStyles, getBorderWidthStyle, getNodeBaseStyle } from './network-modification-node.styles'; +import { DETAIL_LEVELS } from '../zoom.constants'; const NetworkModificationNode = (props: NodeProps) => { const currentNode = useSelector((state: AppState) => state.currentTreeNode); @@ -93,19 +35,29 @@ const NetworkModificationNode = (props: NodeProps) => { const currentRootNetworkUuid = useSelector((state: AppState) => state.currentRootNetworkUuid); const intl = useIntl(); - - const isSelectedNode = () => { + const theme = useTheme(); + const isSelectedNode = useCallback(() => { return props.id === currentNode?.id; - }; + }, [currentNode, props.id]); - const isSelectedForCut = () => { + const isSelectedForCut = useCallback(() => { return ( (props.id === selectionForCopy?.nodeId && selectionForCopy?.copyType === CopyType.NODE_CUT) || ((props.id === selectionForCopy?.nodeId || selectionForCopy.allChildren?.map((child) => child.id)?.includes(props.id)) && selectionForCopy?.copyType === CopyType.SUBTREE_CUT) ); - }; + }, [props.id, selectionForCopy]); + + const nodeOpacity = useMemo(() => { + if (!isSelectedForCut()) { + return 1; + } + return getLocalStorageTheme() === LIGHT_THEME ? 0.3 : 0.6; + }, [isSelectedForCut]); + + const tooltipDelay = theme.tree?.detailLevel === DETAIL_LEVELS.MINIMAL ? 0 : TOOLTIP_DELAY; + const tooltipContent = useMemo(() => { return ( @@ -123,63 +75,65 @@ const NetworkModificationNode = (props: NodeProps) => { ); }, [props.data, intl]); - const getNodeOpacity = () => { - return isSelectedForCut() ? (getLocalStorageTheme() === LIGHT_THEME ? 0.3 : 0.6) : 'unset'; - }; - return ( <> - {props.data.globalBuildStatus !== props.data.localBuildStatus && ( - } - onClick={(e) => e.stopPropagation()} - /> - )} + {props.data.globalBuildStatus !== props.data.localBuildStatus && + zoomStyles.visibility.showGlobalBuildStatus(theme) && ( + } + onClick={(e) => e.stopPropagation()} + /> + )} getNodeBaseStyle(theme, isSelectedNode()), + { opacity: nodeOpacity }, + (theme) => getBorderWidthStyle(theme, isSelectedNode()), ]} > - - + + {props.data.label} - - {props.data.globalBuildStatus !== BUILD_STATUS.BUILDING && ( - - )} - - - - {props.data.localBuildStatus !== BUILD_STATUS.BUILDING && ( - - )} + + + {props.data.globalBuildStatus !== BUILD_STATUS.BUILDING && ( + + )} + + + {props.data.localBuildStatus !== BUILD_STATUS.BUILDING && ( + + )} + {props.data.localBuildStatus === BUILD_STATUS.BUILDING && } diff --git a/src/components/graph/nodes/node-handle.tsx b/src/components/graph/nodes/node-handle.tsx index abe11d9f96..3290f464fd 100644 --- a/src/components/graph/nodes/node-handle.tsx +++ b/src/components/graph/nodes/node-handle.tsx @@ -7,24 +7,29 @@ import { useTheme } from '@mui/material'; import { Handle, HandleType, Position } from '@xyflow/react'; +import { zoomStyles } from '../zoom.styles'; type NodeHandleProps = { type: HandleType; position: Position; }; + const NodeHandle = ({ type, position }: NodeHandleProps) => { const theme = useTheme(); + const hidden = !zoomStyles.visibility.showHandles(theme); + return (