Files
twenty/packages/twenty-front/src/modules/side-panel/components/SidePanelForDesktop.tsx
T
Félix Malfait bfefcd3755 feat(twenty-front): generalize the page primary/secondary bars (flat redesign) (#21308)
Replaces #21279 and #21282 with one clean PR from `main`.

Generalizes the settings primary-bar / secondary-bar card chrome to the
record index, record show and standalone pages via a shared
`PageCardLayout` + `PageCardHeader` (the side panel sits as a sibling of
the content card), and applies the new flat design direction: square
corners on the card, side panel and loading skeletons.

Iterating toward the new design (Figma node 102282-221623); the
confirmed direction and the explicit "remove rounded corners" change are
in, remaining designer specifics to follow.
2026-06-08 22:47:05 +02:00

155 lines
5.3 KiB
TypeScript

import { tableWidthResizeIsActiveState } from '@/object-record/record-table/states/tableWidthResizeIsActivedState';
import { SidePanelRouter } from '@/side-panel/components/SidePanelRouter';
import { SidePanelWidthEffect } from '@/side-panel/components/SidePanelWidthEffect';
import { SIDE_PANEL_CLICK_OUTSIDE_ID } from '@/side-panel/constants/SidePanelClickOutsideId';
import { SIDE_PANEL_CONSTRAINTS } from '@/side-panel/constants/SidePanelConstraints';
import { useSidePanelCloseAnimationCompleteCleanup } from '@/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import {
SIDE_PANEL_WIDTH_VAR,
sidePanelWidthState,
} from '@/side-panel/states/sidePanelWidthState';
import { ModalContainerContext } from '@/ui/layout/modal/contexts/ModalContainerContext';
import { ResizablePanelGap } from '@/ui/layout/resizable-panel/components/ResizablePanelGap';
import { ParentClickOutsideIdContext } from '@/ui/utilities/pointer-event/contexts/ParentClickOutsideIdContext';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { styled } from '@linaria/react';
import { useCallback, useState } from 'react';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
const StyledSidePanelWrapper = styled.div<{
isOpen: boolean;
isResizing: boolean;
}>`
flex-shrink: 0;
min-width: 0;
overflow: hidden;
transition: ${({ isResizing }) =>
isResizing
? 'none'
: `width calc(${themeCssVariables.animation.duration.normal} * 1s)`};
width: ${({ isOpen }) => (isOpen ? `var(${SIDE_PANEL_WIDTH_VAR})` : '0px')};
`;
const StyledSidePanel = styled.aside`
background: ${themeCssVariables.background.primary};
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
position: relative;
width: 100%;
`;
const StyledModalContainer = styled.div`
height: 100%;
left: 0;
pointer-events: none;
position: absolute;
top: 0;
width: 100%;
z-index: 1;
`;
export const SidePanelForDesktop = () => {
const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
const isSidePanelClosing = useAtomStateValue(isSidePanelClosingState);
const [sidePanelWidth, setSidePanelWidth] = useAtomState(sidePanelWidthState);
const { closeSidePanelMenu } = useSidePanelMenu();
const { sidePanelCloseAnimationCompleteCleanup } =
useSidePanelCloseAnimationCompleteCleanup();
const [modalContainer, setModalContainer] = useState<HTMLDivElement | null>(
null,
);
const [isResizing, setIsResizing] = useState(false);
const [shouldRenderContent, setShouldRenderContent] =
useState(isSidePanelOpened);
const setTableWidthResizeIsActive = useSetAtomState(
tableWidthResizeIsActiveState,
);
const shouldShowContent = isSidePanelOpened || shouldRenderContent;
const handleTransitionEnd = () => {
if (isSidePanelOpened) {
// Open animation completed - ensure content persists for close animation
setShouldRenderContent(true);
} else {
// Close animation completed
setShouldRenderContent(false);
if (isSidePanelClosing) {
sidePanelCloseAnimationCompleteCleanup();
}
}
};
const handleModalContainerRef = useCallback(
(element: HTMLDivElement | null) => {
setModalContainer(element);
},
[],
);
const handleWidthChange = useCallback(
(width: number) => {
setSidePanelWidth(width);
setIsResizing(false);
setTableWidthResizeIsActive(true);
},
[setSidePanelWidth, setTableWidthResizeIsActive],
);
const handleResizeStart = useCallback(() => {
setIsResizing(true);
setTableWidthResizeIsActive(false);
}, [setTableWidthResizeIsActive]);
const handleCollapse = useCallback(() => {
closeSidePanelMenu();
setIsResizing(false);
setTableWidthResizeIsActive(true);
}, [closeSidePanelMenu, setTableWidthResizeIsActive]);
return (
<>
<SidePanelWidthEffect />
<ResizablePanelGap
side="left"
constraints={SIDE_PANEL_CONSTRAINTS}
currentWidth={sidePanelWidth}
onWidthChange={handleWidthChange}
onCollapse={handleCollapse}
gapWidth={0}
cssVariableName={SIDE_PANEL_WIDTH_VAR}
onResizeStart={handleResizeStart}
/>
<StyledSidePanelWrapper
isOpen={isSidePanelOpened}
isResizing={isResizing}
onTransitionEnd={handleTransitionEnd}
data-side-panel=""
data-click-outside-id={SIDE_PANEL_CLICK_OUTSIDE_ID}
>
<StyledSidePanel>
<StyledModalContainer ref={handleModalContainerRef} />
<ModalContainerContext.Provider value={{ container: modalContainer }}>
<ParentClickOutsideIdContext.Provider
value={SIDE_PANEL_CLICK_OUTSIDE_ID}
>
{shouldShowContent && <SidePanelRouter />}
</ParentClickOutsideIdContext.Provider>
</ModalContainerContext.Provider>
</StyledSidePanel>
</StyledSidePanelWrapper>
</>
);
};