Skip to content

Commit c703c70

Browse files
committed
Add zoom adaptive styling for nodes tree
Signed-off-by: Ayoub LABIDI <[email protected]>
1 parent 0757e28 commit c703c70

17 files changed

+472
-215
lines changed

src/components/app-wrapper.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ const lightTheme = createTheme({
208208
reactflow: {
209209
backgroundColor: 'white',
210210
labeledGroup: {
211-
backgroundColor: 'white',
212-
borderColor: '#11161A',
211+
backgroundColor: '#FAFAFA',
212+
borderColor: '#BDBDBD',
213213
},
214214
edge: {
215215
stroke: '#6F767B',

src/components/graph/layout.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { NODE_HEIGHT, NODE_WIDTH } from './nodes/constants';
1010
import { groupIdSuffix, LABELED_GROUP_TYPE } from './nodes/labeled-group-node.type';
1111
import { CurrentTreeNode, isSecurityModificationNode, NetworkModificationNodeType } from './tree-node.type';
1212

13-
const widthSpacing = 70;
14-
const heightSpacing = 90;
13+
const widthSpacing = 110;
14+
const heightSpacing = 140;
1515
export const nodeWidth = NODE_WIDTH + widthSpacing;
1616
export const nodeHeight = NODE_HEIGHT + heightSpacing;
1717
export const snapGrid = [10, nodeHeight]; // Used for drag and drop

src/components/graph/nodes/build-button.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ export const BuildButton = ({
9494
return !buildStatus || buildStatus === BUILD_STATUS.NOT_BUILT ? (
9595
<PlayCircleFilled sx={styles.playColor} />
9696
) : (
97-
<StopCircleOutlined color="primary" />
97+
<StopCircleOutlined
98+
sx={{ color: (theme) => (theme.palette.mode === 'light' ? theme.palette.primary.light : undefined) }}
99+
/>
98100
);
99101
};
100102

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (c) 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
import { type SxStyle } from '@gridsuite/commons-ui';
9+
import { Theme } from '@mui/material';
10+
11+
export const buildStatusChipStyles = {
12+
base: (theme: Theme) =>
13+
({
14+
padding: theme.spacing(1, 0.5),
15+
fontSize: '12px',
16+
fontWeight: 400,
17+
lineHeight: '100%',
18+
}) as const,
19+
20+
notBuilt: {
21+
background: (theme: Theme) => theme.node.buildStatus.notBuilt,
22+
color: (theme: Theme) => theme.palette.getContrastText(theme.node.buildStatus.notBuilt),
23+
} as SxStyle,
24+
} as const;

src/components/graph/nodes/build-status-chip.tsx

Lines changed: 30 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,53 +6,26 @@
66
*/
77

88
import React, { ReactElement } from 'react';
9-
import { Chip } from '@mui/material';
9+
import { Chip, useTheme } from '@mui/material';
1010
import { useIntl } from 'react-intl';
1111
import { BUILD_STATUS } from 'components/network/constants';
1212
import { mergeSx, type SxStyle } from '@gridsuite/commons-ui';
13-
14-
function getBuildStatusSx(buildStatus: BUILD_STATUS | undefined): SxStyle {
15-
return (theme) => {
16-
const bs = theme.node.buildStatus;
17-
// pick background based on status
18-
let bg: string;
19-
20-
switch (buildStatus) {
21-
case BUILD_STATUS.BUILT:
22-
bg = bs.success;
23-
break;
24-
case BUILD_STATUS.BUILT_WITH_WARNING:
25-
bg = bs.warning;
26-
break;
27-
case BUILD_STATUS.BUILT_WITH_ERROR:
28-
bg = bs.error;
29-
break;
30-
default:
31-
bg = bs.notBuilt;
32-
break;
33-
}
34-
35-
// only set explicit contrast color when it's the "notBuilt" background
36-
const shouldSetContrast = bg === bs.notBuilt;
37-
38-
return {
39-
background: bg,
40-
...(shouldSetContrast ? { color: theme.palette.getContrastText(bg) } : {}),
41-
'&:hover': {
42-
backgroundColor: bg,
43-
},
44-
};
45-
};
13+
import { zoomStyles } from '../zoom.styles';
14+
import { buildStatusChipStyles } from './build-status-chip.styles';
15+
16+
function getBuildStatusColor(buildStatus: BUILD_STATUS | undefined) {
17+
switch (buildStatus) {
18+
case BUILD_STATUS.BUILT:
19+
return 'success';
20+
case BUILD_STATUS.BUILT_WITH_WARNING:
21+
return 'warning';
22+
case BUILD_STATUS.BUILT_WITH_ERROR:
23+
return 'error';
24+
default:
25+
return undefined;
26+
}
4627
}
4728

48-
const baseStyle: SxStyle = (theme) =>
49-
({
50-
padding: theme.spacing(1, 0.5),
51-
fontSize: '12px',
52-
fontWeight: 400,
53-
lineHeight: '100%',
54-
}) as const;
55-
5629
type BuildStatusChipProps = {
5730
buildStatus?: BUILD_STATUS;
5831
sx?: SxStyle;
@@ -62,18 +35,24 @@ type BuildStatusChipProps = {
6235

6336
const BuildStatusChip = ({ buildStatus = BUILD_STATUS.NOT_BUILT, sx, icon, onClick }: BuildStatusChipProps) => {
6437
const intl = useIntl();
38+
const theme = useTheme();
39+
40+
const showLabel = zoomStyles.visibility.showBuildStatusLabel(theme);
41+
const label = showLabel ? intl.formatMessage({ id: buildStatus }) : undefined;
42+
const color = getBuildStatusColor(buildStatus);
6543

66-
const label = intl.formatMessage({ id: buildStatus });
44+
// Custom styling for NOT_BUILT status (no standard MUI color)
45+
const notBuiltSx: SxStyle | undefined = color === undefined ? buildStatusChipStyles.notBuilt : undefined;
6746

68-
return (
69-
<Chip
70-
label={label}
71-
size="small"
72-
icon={icon}
73-
onClick={onClick}
74-
sx={mergeSx(getBuildStatusSx(buildStatus), sx, baseStyle)}
75-
/>
47+
// Combine styles: base + compact circular (if no label) + notBuilt color + parent overrides
48+
const finalSx = mergeSx(
49+
buildStatusChipStyles.base,
50+
showLabel ? undefined : (theme) => zoomStyles.layout.getCompactChipSize(theme),
51+
notBuiltSx,
52+
sx
7653
);
54+
55+
return <Chip label={label} size="small" icon={icon} onClick={onClick} color={color} sx={finalSx} />;
7756
};
7857

7958
export default BuildStatusChip;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Copyright (c) 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
import { type MuiStyles } from '@gridsuite/commons-ui';
9+
import { colors, Theme } from '@mui/material';
10+
import { zoomStyles } from '../zoom.styles';
11+
12+
export const LABEL_BLOCK_HEIGHT = 30; // px, matches label box vertical size
13+
14+
export const getContainerStyle = (theme: Theme, isLight: boolean) => ({
15+
...zoomStyles.labeledGroupBorder(theme),
16+
background: theme.tree?.is.minimalDetail ? (isLight ? theme.palette.grey[200] : colors.grey[700]) : 'transparent',
17+
borderColor: isLight ? colors.grey[400] : colors.grey[500],
18+
});
19+
20+
export const labeledGroupNodeStyles = {
21+
label: (theme) => ({
22+
position: 'absolute',
23+
top: -15,
24+
right: 20,
25+
backgroundColor: theme.reactflow.labeledGroup.backgroundColor,
26+
padding: '3px 6px',
27+
border: '1px solid',
28+
borderColor: theme.reactflow.labeledGroup.borderColor,
29+
boxShadow: theme.shadows[1],
30+
fontSize: '12px',
31+
display: theme.tree?.atMost.minimalDetail ? 'none' : 'flex',
32+
gap: '4px',
33+
alignItems: 'center',
34+
justifyContent: 'center',
35+
lineHeight: 1,
36+
}),
37+
38+
icon: (theme) => ({
39+
fontSize: zoomStyles.iconSize(theme),
40+
verticalAlign: 'middle',
41+
}),
42+
43+
text: (theme) => ({
44+
display: theme.tree?.atLeast.standardDetail ? 'inline' : 'none',
45+
verticalAlign: 'middle',
46+
}),
47+
} as const satisfies MuiStyles;

src/components/graph/nodes/labeled-group-node.tsx

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,50 +5,34 @@
55
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
66
*/
77

8-
import { Box } from '@mui/material';
9-
import { NodeProps, ReactFlowState, useStore } from '@xyflow/react';
10-
import { nodeHeight as nodeLayoutHeight, nodeWidth as nodeLayoutWidth } from '../layout';
8+
import { Box, useTheme } from '@mui/material';
9+
import { NodeProps } from '@xyflow/react';
1110
import SecurityIcon from '@mui/icons-material/Security';
11+
import { nodeHeight as nodeLayoutHeight, nodeWidth as nodeLayoutWidth } from '../layout';
1212
import { FormattedMessage } from 'react-intl';
13-
import { type MuiStyles } from '@gridsuite/commons-ui';
1413
import { LabeledGroupNodeType } from './labeled-group-node.type';
1514
import { NODE_HEIGHT, NODE_WIDTH } from './constants';
16-
17-
const styles = {
18-
border: {
19-
border: 'dashed 3px #8B8F8F',
20-
borderRadius: '8px',
21-
},
22-
label: (theme) => ({
23-
position: 'absolute',
24-
top: -13,
25-
right: 8,
26-
backgroundColor: theme.reactflow.labeledGroup.backgroundColor,
27-
padding: '0px 6px',
28-
border: '1px solid',
29-
borderColor: theme.reactflow.labeledGroup.borderColor,
30-
fontSize: 12,
31-
display: 'flex',
32-
gap: '10px',
33-
alignItems: 'center',
34-
}),
35-
} as const satisfies MuiStyles;
15+
import { labeledGroupNodeStyles, getContainerStyle, LABEL_BLOCK_HEIGHT } from './labeled-group-node.styles';
3616

3717
export function LabeledGroupNode({ data }: NodeProps<LabeledGroupNodeType>) {
18+
const theme = useTheme();
3819
// Vertically, the border is halfway between the node and the edge above,
3920
// and since that edge is centered between two nodes, we divide the space by 4.
4021
const verticalPadding = (nodeLayoutHeight - NODE_HEIGHT) / 4;
4122
// horizontally, the border is placed exactly halfway between two nodes — that's why we divide the space between them by 2.
4223
const horizontalPadding = (nodeLayoutWidth - NODE_WIDTH) / 2;
4324

44-
const labeledGroupTopPosition = data.position.topLeft.row * nodeLayoutHeight - verticalPadding;
25+
// Adjust position and size to account for border width and label block
26+
const labeledGroupTopPosition = data.position.topLeft.row * nodeLayoutHeight - verticalPadding - LABEL_BLOCK_HEIGHT;
4527
const labeledGroupLeftPosition = data.position.topLeft.column * nodeLayoutWidth - horizontalPadding;
4628

4729
const labeledGroupHeight =
48-
(data.position.bottomRight.row - data.position.topLeft.row + 1) * nodeLayoutHeight - 2 * verticalPadding;
30+
(data.position.bottomRight.row - data.position.topLeft.row + 1) * nodeLayoutHeight -
31+
2 * verticalPadding +
32+
2 * LABEL_BLOCK_HEIGHT;
4933
const labeledGroupWidth = (data.position.bottomRight.column - data.position.topLeft.column + 1) * nodeLayoutWidth;
5034

51-
const zoom = useStore((s: ReactFlowState) => s.transform?.[2] ?? 1);
35+
const isLight = theme.palette.mode === 'light';
5236

5337
return (
5438
<Box
@@ -58,14 +42,14 @@ export function LabeledGroupNode({ data }: NodeProps<LabeledGroupNodeType>) {
5842
left={labeledGroupLeftPosition}
5943
height={labeledGroupHeight}
6044
width={labeledGroupWidth}
61-
sx={styles.border}
45+
sx={getContainerStyle(theme, isLight)}
6246
>
63-
{zoom >= 0.5 && (
64-
<Box sx={styles.label}>
65-
<SecurityIcon sx={{ fontSize: '12px' }} />
47+
<Box sx={labeledGroupNodeStyles.label}>
48+
<SecurityIcon sx={labeledGroupNodeStyles.icon} />
49+
<Box component="span" sx={labeledGroupNodeStyles.text}>
6650
<FormattedMessage id="labeledGroupSecurity" />
6751
</Box>
68-
)}
52+
</Box>
6953
</Box>
7054
);
7155
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Copyright (c) 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
import { type MuiStyles } from '@gridsuite/commons-ui';
9+
import { Theme } from '@mui/material';
10+
import { baseNodeStyles, interactiveNodeStyles } from './styles';
11+
import { zoomStyles } from '../zoom.styles';
12+
13+
export const getBorderWidthStyle = (theme: Theme, isSelected: boolean) => ({
14+
borderWidth: zoomStyles.borderWidth(theme, isSelected),
15+
});
16+
17+
export const getNodeBaseStyle = (theme: Theme, isSelected: boolean) =>
18+
isSelected ? modificationNodeStyles.selected(theme) : modificationNodeStyles.default(theme);
19+
20+
export const modificationNodeStyles = {
21+
selected: (theme) => ({
22+
...baseNodeStyles(theme, 'column'),
23+
background: theme.node.modification.selectedBackground,
24+
border: theme.node.modification.selectedBorder,
25+
boxShadow: theme.shadows[6],
26+
...interactiveNodeStyles(theme, 'modification'),
27+
}),
28+
29+
default: (theme) => ({
30+
...baseNodeStyles(theme, 'column'),
31+
border: theme.node.modification.border,
32+
...interactiveNodeStyles(theme, 'modification'),
33+
}),
34+
35+
contentBox: (theme) => ({
36+
flexGrow: 1,
37+
display: zoomStyles.visibility.showNodeContent(theme) ? 'flex' : 'none',
38+
alignItems: 'flex-end',
39+
marginLeft: theme.spacing(1),
40+
marginRight: theme.spacing(1),
41+
marginBottom: theme.spacing(1),
42+
}),
43+
44+
typographyText: (theme) => ({
45+
color: theme.palette.text.primary,
46+
fontSize: '20px',
47+
fontWeight: 400,
48+
lineHeight: 'normal',
49+
textAlign: 'left',
50+
display: '-webkit-box',
51+
WebkitBoxOrient: 'vertical',
52+
WebkitLineClamp: 2,
53+
overflow: 'hidden',
54+
width: 'auto',
55+
textOverflow: 'ellipsis',
56+
wordBreak: 'break-word',
57+
}),
58+
59+
footer: (theme) => ({
60+
display: 'flex',
61+
justifyContent: 'space-between',
62+
alignItems: 'center',
63+
marginLeft: theme.spacing(1),
64+
marginRight: theme.spacing(1),
65+
height: zoomStyles.layout.useFullHeightFooter(theme) ? '100%' : '35%',
66+
}),
67+
68+
chipFloating: (theme) => ({
69+
position: 'absolute',
70+
top: theme.spacing(-4.3),
71+
left: theme.spacing(1),
72+
zIndex: 2,
73+
}),
74+
75+
chipLarge: (theme) => zoomStyles.layout.getLargeChipSize(theme) ?? {},
76+
77+
buildButtonContainer: (theme) => ({
78+
display: zoomStyles.visibility.showBuildButton(theme) ? 'block' : 'none',
79+
}),
80+
81+
globalBuildStatusIcon: (theme) => ({
82+
fontSize: zoomStyles.iconSize(theme),
83+
'& path': {
84+
stroke: `currentColor`,
85+
strokeWidth: `${zoomStyles.iconStrokeWidth(theme)}px`,
86+
},
87+
}),
88+
89+
tooltip: {
90+
maxWidth: '720px',
91+
},
92+
} as const satisfies MuiStyles;

0 commit comments

Comments
 (0)