Files
twenty/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx
T
Abdul Rahman 04b0a65e73 feat: fix Command Menu Side Panel Layout (#15883)
[Figma
Design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=81380-344641&t=FpjWNOK2gZuDQQfr-0)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds a side panel layout for the Command Menu, routes modals into a
local container, updates the top bar and context chips, and standardizes
small button sizes.
> 
> - **Command Menu**:
> - **Side Panel Layout**: Introduces `CommandMenuSidePanelLayout` with
animated width, hosts `CommandMenuRouter`, and provides a modal
container via `ModalContainerContext`.
> - **Top Bar**: Redesign (`CommandMenuTopBar`) with back icon, optional
AI sparkles action, compact height
(`COMMAND_MENU_SEARCH_BAR_HEIGHT=40`), and updated placeholder.
> - **Context Chips**: Adds `CommandMenuLastContextChip` and
`CommandMenuRecordInfo`; extends `CommandMenuContextChip` with `page`
prop; updates `CommandMenuContextChipGroups` to render last chip as
record info when applicable.
> - **Container Simplification**: `CommandMenuContainer` simplified to
just provide contexts and `AgentChatProvider`.
> - **Modal System**:
> - Adds `ModalContainerContext` and updates `Modal` to portal into
provided container; `Modal.Backdrop` supports `isInContainer`.
> - Updates usages (e.g., `UserOrMetadataLoader`, `ActionModal`) to
align with new modal behavior.
> - **Page Integration**:
> - Replaces `PageBody` with `CommandMenuSidePanelLayout` in
`RecordShowPage` and `RecordIndexContainerGater`.
> - Removes global `CommandMenuRouter` from `DefaultLayout` (keeps
keyboard shortcuts).
> - **UI/Styling**:
> - Standardizes several buttons to `size="small"` (e.g., command
actions, open record, options, reply, workflow footer).
> - Adjusts `ShowPageSubContainer` styling when rendered inside command
menu.
>   - Storybook tests updated for new placeholder text.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
81fcaa145618a2fa1c3e11e2dc88fe832c51374c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
Co-authored-by: Aman Raj <92664006+araj00@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
2025-11-21 16:12:41 +01:00

165 lines
5.5 KiB
TypeScript

import { CommandMenuBackButton } from '@/command-menu/components/CommandMenuBackButton';
import { CommandMenuPageInfo } from '@/command-menu/components/CommandMenuPageInfo';
import { CommandMenuTopBarInputFocusEffect } from '@/command-menu/components/CommandMenuTopBarInputFocusEffect';
import { CommandMenuTopBarRightCornerIcon } from '@/command-menu/components/CommandMenuTopBarRightCornerIcon';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE } from '@/command-menu/constants/CommandMenuSearchBarHeightMobile';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuContextChips } from '@/command-menu/hooks/useCommandMenuContextChips';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { AnimatePresence, motion } from 'framer-motion';
import { useRef } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { IconX } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
const StyledInputContainer = styled.div<{ isMobile: boolean }>`
align-items: center;
background-color: ${({ theme }) => theme.background.secondary};
border: none;
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: 0;
box-sizing: border-box;
display: flex;
justify-content: space-between;
font-size: ${({ theme }) => theme.font.size.lg};
height: ${({ isMobile }) =>
isMobile
? COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE
: COMMAND_MENU_SEARCH_BAR_HEIGHT}px;
margin: 0;
outline: none;
position: relative;
overflow: hidden;
padding: 0 ${({ theme }) => theme.spacing(COMMAND_MENU_SEARCH_BAR_PADDING)};
gap: ${({ theme }) => theme.spacing(4)};
flex-shrink: 0;
justify-content: space-between;
`;
const StyledInput = styled.input`
border: none;
border-radius: 0;
background-color: transparent;
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
margin: 0;
outline: none;
height: 24px;
padding: 0;
flex: 1;
&::placeholder {
color: ${({ theme }) => theme.font.color.light};
font-weight: ${({ theme }) => theme.font.weight.medium};
}
`;
const StyledContentContainer = styled.div`
align-items: center;
display: flex;
flex: 1;
gap: ${({ theme }) => theme.spacing(1)};
min-width: 0;
overflow: hidden;
`;
export const CommandMenuTopBar = () => {
const [commandMenuSearch, setCommandMenuSearch] = useRecoilState(
commandMenuSearchState,
);
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useLingui();
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCommandMenuSearch(event.target.value);
};
const isMobile = useIsMobile();
const { closeCommandMenu } = useCommandMenu();
const commandMenuPage = useRecoilValue(commandMenuPageState);
const commandMenuNavigationStack = useRecoilValue(
commandMenuNavigationStackState,
);
const theme = useTheme();
const { contextChips } = useCommandMenuContextChips();
const canGoBack = commandMenuNavigationStack.length > 1;
const shouldShowCloseButton =
!isMobile && commandMenuNavigationStack.length === 1;
const shouldShowBackButton = canGoBack;
const lastChip = contextChips.at(-1);
return (
<StyledInputContainer isMobile={isMobile}>
<StyledContentContainer>
<AnimatePresence>
{shouldShowBackButton && (
<motion.div
exit={{ opacity: 0, width: 0 }}
transition={{
duration: theme.animation.duration.instant,
}}
>
<CommandMenuBackButton />
</motion.div>
)}
{shouldShowCloseButton && (
<motion.div
exit={{ opacity: 0, width: 0 }}
transition={{
duration: theme.animation.duration.instant,
}}
>
<IconButton
Icon={IconX}
size="small"
variant="tertiary"
onClick={closeCommandMenu}
/>
</motion.div>
)}
</AnimatePresence>
{lastChip &&
commandMenuPage !== CommandMenuPages.Root &&
commandMenuPage !== CommandMenuPages.SearchRecords && (
<CommandMenuPageInfo pageChip={lastChip} />
)}
{(commandMenuPage === CommandMenuPages.Root ||
commandMenuPage === CommandMenuPages.SearchRecords) && (
<>
<StyledInput
data-testid="command-menu-search-input"
ref={inputRef}
value={commandMenuSearch}
placeholder={t`Type anything...`}
onChange={handleSearchChange}
/>
<CommandMenuTopBarInputFocusEffect inputRef={inputRef} />
</>
)}
</StyledContentContainer>
<CommandMenuTopBarRightCornerIcon />
</StyledInputContainer>
);
};