04b0a65e73
[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>
165 lines
5.5 KiB
TypeScript
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>
|
|
);
|
|
};
|