diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index a984504d1acc..98d096cee788 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -11,11 +11,11 @@ import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect'; import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect'; import { Authorize } from '~/pages/auth/Authorize'; -import { ChooseYourPlan } from '~/pages/auth/ChooseYourPlan.tsx'; +import { ChooseYourPlan } from '~/pages/auth/ChooseYourPlan'; import { CreateProfile } from '~/pages/auth/CreateProfile'; import { CreateWorkspace } from '~/pages/auth/CreateWorkspace'; import { PasswordReset } from '~/pages/auth/PasswordReset'; -import { PaymentSuccess } from '~/pages/auth/PaymentSuccess.tsx'; +import { PaymentSuccess } from '~/pages/auth/PaymentSuccess'; import { SignInUp } from '~/pages/auth/SignInUp'; import { DefaultHomePage } from '~/pages/DefaultHomePage'; import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect'; @@ -46,7 +46,7 @@ import { SettingsIntegrationNewDatabaseConnection } from '~/pages/settings/integ import { SettingsIntegrations } from '~/pages/settings/integrations/SettingsIntegrations'; import { SettingsIntegrationShowDatabaseConnection } from '~/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection'; import { SettingsAppearance } from '~/pages/settings/SettingsAppearance'; -import { SettingsBilling } from '~/pages/settings/SettingsBilling.tsx'; +import { SettingsBilling } from '~/pages/settings/SettingsBilling'; import { SettingsProfile } from '~/pages/settings/SettingsProfile'; import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace'; import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers'; diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventParticipantsResponseStatusField.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventParticipantsResponseStatusField.tsx index 6a83a6f723b5..9c49e1f6b6f2 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventParticipantsResponseStatusField.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventParticipantsResponseStatusField.tsx @@ -1,14 +1,15 @@ -import { useRef } from 'react'; +import { useRef, useState } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; import { IconCheck, IconQuestionMark, IconX } from 'twenty-ui'; -import { v4 } from 'uuid'; import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant'; import { ParticipantChip } from '@/activities/components/ParticipantChip'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; -import { ExpandableList } from '@/ui/display/expandable-list/ExpandableList'; import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay'; +import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList'; +import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted'; const StyledInlineCellBaseContainer = styled.div` align-items: center; @@ -55,6 +56,9 @@ const StyledLabelContainer = styled.div<{ width?: number }>` font-size: ${({ theme }) => theme.font.size.sm}; width: ${({ width }) => width}px; `; +const StyledDiv = styled.div` + max-width: 70%; +`; export const CalendarEventParticipantsResponseStatusField = ({ responseStatus, @@ -64,6 +68,9 @@ export const CalendarEventParticipantsResponseStatusField = ({ participants: CalendarEventParticipant[]; }) => { const theme = useTheme(); + const isRightDrawerAnimationCompleted = useRecoilValue( + isRightDrawerAnimationCompletedState, + ); const Icon = { Yes: , @@ -81,9 +88,9 @@ export const CalendarEventParticipantsResponseStatusField = ({ ]; const participantsContainerRef = useRef(null); - - const StyledChips = orderedParticipants.map((participant) => ( - + const [isHovered, setIsHovered] = useState(false); + const styledChips = orderedParticipants.map((participant, index) => ( + )); return ( @@ -96,12 +103,21 @@ export const CalendarEventParticipantsResponseStatusField = ({ {responseStatus} - - + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {isRightDrawerAnimationCompleted && ( + + {styledChips} + + )} + ); diff --git a/packages/twenty-front/src/modules/activities/components/ActivityEditorFields.tsx b/packages/twenty-front/src/modules/activities/components/ActivityEditorFields.tsx index 31ae39ceef38..fe5e37539495 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityEditorFields.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityEditorFields.tsx @@ -14,6 +14,7 @@ import { import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted'; import { isDefined } from '~/utils/isDefined'; const StyledPropertyBox = styled(PropertyBox)` @@ -27,6 +28,10 @@ export const ActivityEditorFields = ({ }) => { const { upsertActivity } = useUpsertActivity(); + const isRightDrawerAnimationCompleted = useRecoilValue( + isRightDrawerAnimationCompletedState, + ); + const getRecordFromCache = useGetRecordFromCache({ objectNameSingular: CoreObjectNameSingular.Activity, }); @@ -93,11 +98,16 @@ export const ActivityEditorFields = ({ )} - {ActivityTargetsContextProvider && isDefined(activityFromCache) && ( - - - - )} + {ActivityTargetsContextProvider && + isDefined(activityFromCache) && + isRightDrawerAnimationCompleted && ( + + + + )} ); }; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx b/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx index ace772939d68..3a46146fe529 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx @@ -1,91 +1,47 @@ -import { useMemo } from 'react'; import styled from '@emotion/styled'; -import { Chip, ChipVariant } from 'twenty-ui'; -import { v4 } from 'uuid'; import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject'; import { RecordChip } from '@/object-record/components/RecordChip'; -import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { + ExpandableList, + ExpandableListProps, +} from '@/ui/layout/expandable-list/components/ExpandableList'; -const MAX_RECORD_CHIPS_DISPLAY = 2; - -const StyledContainer = styled.div` +const StyledContainer = styled.div<{ maxWidth?: number }>` display: flex; flex-wrap: wrap; gap: ${({ theme }) => theme.spacing(1)}; + max-width: ${({ maxWidth }) => `${maxWidth}px` || 'none'}; `; -const StyledRelationsListContainer = styled(StyledContainer)` - padding: ${({ theme }) => theme.spacing(2)}; - border-radius: ${({ theme }) => theme.spacing(1)}; - background-color: ${({ theme }) => theme.background.secondary}; - box-shadow: '0px 2px 4px ${({ theme }) => - theme.boxShadow.light}, 2px 4px 16px ${({ theme }) => - theme.boxShadow.strong}'; - backdrop-filter: ${({ theme }) => theme.blur.strong}; -`; - -const showMoreRelationsHandler = (event?: React.MouseEvent) => { - event?.preventDefault(); - event?.stopPropagation(); -}; - export const ActivityTargetChips = ({ activityTargetObjectRecords, + isHovered, + reference, + maxWidth, }: { activityTargetObjectRecords: ActivityTargetWithTargetRecord[]; -}) => { - const dropdownId = useMemo(() => `multiple-relations-dropdown-${v4()}`, []); - + maxWidth?: number; +} & ExpandableListProps) => { return ( - - {activityTargetObjectRecords - ?.slice(0, MAX_RECORD_CHIPS_DISPLAY) - .map((activityTargetObjectRecord) => ( - - ))} - - {activityTargetObjectRecords.length > MAX_RECORD_CHIPS_DISPLAY && ( -
- - } - dropdownOffset={{ x: 0, y: -20 }} - dropdownComponents={ - - {activityTargetObjectRecords.map( - (activityTargetObjectRecord) => ( - - ), - )} - - } - /> -
- )} + + + {activityTargetObjectRecords.map( + (activityTargetObjectRecord, index) => ( + + ), + )} + ); }; diff --git a/packages/twenty-front/src/modules/activities/components/ParticipantChip.tsx b/packages/twenty-front/src/modules/activities/components/ParticipantChip.tsx index 4ba26346fefc..f0242f67ea69 100644 --- a/packages/twenty-front/src/modules/activities/components/ParticipantChip.tsx +++ b/packages/twenty-front/src/modules/activities/components/ParticipantChip.tsx @@ -39,6 +39,7 @@ const StyledChip = styled.div` padding: ${({ theme }) => theme.spacing(1)}; height: 20px; box-sizing: border-box; + white-space: nowrap; `; type ParticipantChipVariant = 'default' | 'bold'; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx index 0ba763525629..21310d09aabc 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx @@ -13,10 +13,16 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; type ActivityTargetsInlineCellProps = { activity: Activity; + showLabel?: boolean; + maxWidth?: number; + readonly?: boolean; }; export const ActivityTargetsInlineCell = ({ activity, + showLabel = true, + maxWidth, + readonly, }: ActivityTargetsInlineCellProps) => { const { activityTargetObjectRecords } = useActivityTargetObjectRecords(activity); @@ -37,8 +43,9 @@ export const ActivityTargetsInlineCell = ({ customEditHotkeyScope={{ scope: ActivityEditorHotkeyScope.ActivityTargets, }} - IconLabel={IconArrowUpRight} - showLabel={true} + IconLabel={showLabel ? IconArrowUpRight : undefined} + showLabel={showLabel} + readonly={readonly} editModeContent={ } isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0} diff --git a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx index 3ae87c635959..caefb987a80d 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx @@ -100,7 +100,7 @@ export const NoteCard = ({ {body} - + {note.comments && note.comments.length > 0 && ( diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx index a7ad9927ce82..7ba8edb00276 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx @@ -6,9 +6,8 @@ import { OverflowingTextWithTooltip, } from 'twenty-ui'; -import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips'; -import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; +import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; import { Activity } from '@/activities/types/Activity'; import { getActivitySummary } from '@/activities/utils/getActivitySummary'; import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox'; @@ -18,7 +17,7 @@ import { useCompleteTask } from '../hooks/useCompleteTask'; const StyledContainer = styled.div` align-items: center; - align-self: stretch; + justify-content: space-between; border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; cursor: pointer; display: inline-flex; @@ -34,9 +33,6 @@ const StyledContainer = styled.div` const StyledTaskBody = styled.div` color: ${({ theme }) => theme.font.color.tertiary}; display: flex; - flex-direction: row; - flex-grow: 1; - width: 1px; `; const StyledTaskTitle = styled.div<{ @@ -64,9 +60,10 @@ const StyledDueDate = styled.div<{ display: flex; gap: ${({ theme }) => theme.spacing(1)}; padding-left: ${({ theme }) => theme.spacing(2)}; + white-space: nowrap; `; -const StyledFieldsContainer = styled.div` +const StyledRightSideContainer = styled.div` display: flex; `; @@ -74,6 +71,14 @@ const StyledPlaceholder = styled.div` color: ${({ theme }) => theme.font.color.light}; `; +const StyledLeftSideContainer = styled.div` + display: flex; +`; + +const StyledCheckboxContainer = styled.div` + display: flex; +`; + export const TaskRow = ({ task }: { task: Activity }) => { const theme = useTheme(); const openActivityRightDrawer = useOpenActivityRightDrawer(); @@ -81,39 +86,42 @@ export const TaskRow = ({ task }: { task: Activity }) => { const body = getActivitySummary(task.body); const { completeTask } = useCompleteTask(task); - const { activityTargetObjectRecords } = useActivityTargetObjectRecords(task); - return ( { openActivityRightDrawer(task.id); }} > -
{ - e.stopPropagation(); - }} - > - -
- - {task.title || Task title} - - - - {task.comments && task.comments.length > 0 && ( - - - - )} - - - + { + e.stopPropagation(); + }} + > + + + + {task.title || Task title} + + + + {task.comments && task.comments.length > 0 && ( + + + + )} + + + + { {task.dueAt && beautifyExactDate(task.dueAt)} - +
); }; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx index b441b7ab77dc..afb561724d23 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx @@ -70,7 +70,7 @@ export const EventsGroup = ({ {group.items.map((event, index) => ( diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx index a5823db43939..80e5d1310576 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx @@ -17,7 +17,7 @@ import { captchaProviderState } from '@/client-config/states/captchaProviderStat import { Loader } from '@/ui/feedback/loader/components/Loader'; import { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInput } from '@/ui/input/components/TextInput'; -import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx'; +import { ActionLink } from '@/ui/navigation/link/components/ActionLink'; import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts index 0a0feb5f6323..45cc945b3a3c 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { useEmailPasswordResetLinkMutation } from '~/generated/graphql.tsx'; +import { useEmailPasswordResetLinkMutation } from '~/generated/graphql'; export const useHandleResetPassword = () => { const { enqueueSnackBar } = useSnackBar(); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts index 7d4336071b18..195a4e44daee 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts @@ -6,7 +6,7 @@ import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState'; import { previousUrlState } from '@/auth/states/previousUrlState'; import { billingState } from '@/client-config/states/billingState'; import { AppPath } from '@/types/AppPath'; -import { WorkspaceMember } from '~/generated/graphql.tsx'; +import { WorkspaceMember } from '~/generated/graphql'; export const useNavigateAfterSignInUp = () => { const navigate = useNavigate(); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithMicrosoft.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithMicrosoft.ts index 06150ec0d821..444bff19d31f 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithMicrosoft.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithMicrosoft.ts @@ -1,6 +1,6 @@ import { useParams } from 'react-router-dom'; -import { useAuth } from '@/auth/hooks/useAuth.ts'; +import { useAuth } from '@/auth/hooks/useAuth'; export const useSignInWithMicrosoft = () => { const workspaceInviteHash = useParams().workspaceInviteHash; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts index a821ecee4590..f5403e9b4a1d 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts @@ -1,6 +1,6 @@ import { useParams } from 'react-router-dom'; -import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql.tsx'; +import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql'; export const useWorkspaceFromInviteHash = () => { const workspaceInviteHash = useParams().workspaceInviteHash; diff --git a/packages/twenty-front/src/modules/billing/components/SubscriptionCard.tsx b/packages/twenty-front/src/modules/billing/components/SubscriptionCard.tsx index 7af7e093bc90..d92da1d823ab 100644 --- a/packages/twenty-front/src/modules/billing/components/SubscriptionCard.tsx +++ b/packages/twenty-front/src/modules/billing/components/SubscriptionCard.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -import { SubscriptionCardPrice } from '@/billing/components/SubscriptionCardPrice.tsx'; +import { SubscriptionCardPrice } from '@/billing/components/SubscriptionCardPrice'; import { capitalize } from '~/utils/string/capitalize'; type SubscriptionCardProps = { diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx index 3c8fee324ce7..57ca920cf8cd 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx @@ -2,6 +2,7 @@ import { useContext } from 'react'; import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; +import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList'; import { FieldContext } from '../contexts/FieldContext'; import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay'; @@ -13,7 +14,7 @@ import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDi import { FullNameFieldDisplay } from '../meta-types/display/components/FullNameFieldDisplay'; import { JsonFieldDisplay } from '../meta-types/display/components/JsonFieldDisplay'; import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay'; -import { MultiSelectFieldDisplay } from '../meta-types/display/components/MultiSelectFieldDisplay.tsx'; +import { MultiSelectFieldDisplay } from '../meta-types/display/components/MultiSelectFieldDisplay'; import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay'; import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay'; import { RelationFieldDisplay } from '../meta-types/display/components/RelationFieldDisplay'; @@ -36,7 +37,13 @@ import { isFieldSelect } from '../types/guards/isFieldSelect'; import { isFieldText } from '../types/guards/isFieldText'; import { isFieldUuid } from '../types/guards/isFieldUuid'; -export const FieldDisplay = () => { +type FieldDisplayProps = ExpandableListProps; + +export const FieldDisplay = ({ + isHovered, + reference, + fromTableCell, +}: FieldDisplayProps & { fromTableCell?: boolean }) => { const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext); const isChipDisplay = @@ -74,7 +81,11 @@ export const FieldDisplay = () => { ) : isFieldSelect(fieldDefinition) ? ( ) : isFieldMultiSelect(fieldDefinition) ? ( - + ) : isFieldAddress(fieldDefinition) ? ( ) : isFieldRawJson(fieldDefinition) ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx index 54a59fd57d8b..f814ab66ce1c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx @@ -4,7 +4,7 @@ import { AddressFieldInput } from '@/object-record/record-field/meta-types/input import { DateFieldInput } from '@/object-record/record-field/meta-types/input/components/DateFieldInput'; import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput'; import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput'; -import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx'; +import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput'; import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput'; import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput'; import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useGetButtonIcon.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useGetButtonIcon.ts index ebec13c24947..035c24c3cbb0 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useGetButtonIcon.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useGetButtonIcon.ts @@ -1,6 +1,7 @@ import { useContext } from 'react'; import { IconComponent, IconPencil } from 'twenty-ui'; +import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -17,7 +18,8 @@ export const useGetButtonIcon = (): IconComponent | undefined => { if ( isFieldLink(fieldDefinition) || isFieldEmail(fieldDefinition) || - isFieldPhone(fieldDefinition) + isFieldPhone(fieldDefinition) || + isFieldMultiSelect(fieldDefinition) ) { return IconPencil; } diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts index 10648a4bd935..9336da879ee5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts @@ -10,7 +10,7 @@ import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/ import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue'; import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; -import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue.ts'; +import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue'; import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx index 5cafe51565b0..bd8bb95a1cdb 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx @@ -1,13 +1,16 @@ -import styled from '@emotion/styled'; - import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField'; import { Tag } from '@/ui/display/tag/components/Tag'; +import { + ExpandableList, + ExpandableListProps, +} from '@/ui/layout/expandable-list/components/ExpandableList'; -const StyledTagContainer = styled.div` - display: flex; - gap: ${({ theme }) => theme.spacing(1)}; -`; -export const MultiSelectFieldDisplay = () => { +type MultiSelectFieldDisplayProps = ExpandableListProps; +export const MultiSelectFieldDisplay = ({ + isHovered, + reference, + withDropDownBorder, +}: MultiSelectFieldDisplayProps) => { const { fieldValues, fieldDefinition } = useMultiSelectField(); const selectedOptions = fieldValues @@ -17,7 +20,11 @@ export const MultiSelectFieldDisplay = () => { : []; return selectedOptions ? ( - + {selectedOptions.map((selectedOption, index) => ( { text={selectedOption.label} /> ))} - +
) : ( <> ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts index 9cd2bed9663f..39816a277e32 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts @@ -9,7 +9,7 @@ import { assertFieldMetadata } from '@/object-record/record-field/types/guards/a import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; -import { FieldMetadataType } from '~/generated/graphql.tsx'; +import { FieldMetadataType } from '~/generated/graphql'; export const useMultiSelectField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx index 0614ba33b866..38c3cca0c588 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx @@ -7,7 +7,7 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; -import { MenuItemMultiSelectTag } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectTag.tsx'; +import { MenuItemMultiSelectTag } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectTag'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx index e3db9d7100f9..185a9924996c 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx @@ -1,12 +1,12 @@ -import { useContext, useState } from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; import { Tooltip } from 'react-tooltip'; import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { motion } from 'framer-motion'; import { IconComponent } from 'twenty-ui'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay'; +import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useInlineCell } from '../hooks/useInlineCell'; @@ -48,11 +48,6 @@ const StyledLabelContainer = styled.div<{ width?: number }>` width: ${({ width }) => width}px; `; -const StyledEditButtonContainer = styled(motion.div)` - align-items: center; - display: flex; -`; - const StyledClickableContainer = styled.div<{ readonly?: boolean }>` display: flex; gap: ${({ theme }) => theme.spacing(1)}; @@ -119,18 +114,24 @@ export const RecordInlineCellContainer = ({ disableHoverEffect, }: RecordInlineCellContainerProps) => { const { entityId, fieldDefinition } = useContext(FieldContext); + const reference = useRef(null); const [isHovered, setIsHovered] = useState(false); + const [isHoveredForDisplayMode, setIsHoveredForDisplayMode] = useState(false); + const [newDisplayModeContent, setNewDisplayModeContent] = + useState(displayModeContent); const handleContainerMouseEnter = () => { if (!readonly) { setIsHovered(true); } + setIsHoveredForDisplayMode(true); }; const handleContainerMouseLeave = () => { if (!readonly) { setIsHovered(false); } + setIsHoveredForDisplayMode(false); }; const { isInlineCellInEditMode, openInlineCell } = useInlineCell(); @@ -151,6 +152,17 @@ export const RecordInlineCellContainer = ({ const theme = useTheme(); const labelId = `label-${entityId}-${fieldDefinition?.metadata?.fieldName}`; + useEffect(() => { + if (React.isValidElement(displayModeContent)) { + setNewDisplayModeContent( + React.cloneElement(displayModeContent, { + isHovered: isHoveredForDisplayMode, + reference: reference.current || undefined, + }), + ); + } + }, [isHoveredForDisplayMode, displayModeContent, reference]); + return ( )} - + {!readonly && isInlineCellInEditMode ? ( {editModeContent} ) : editModeContentOnly ? ( @@ -208,18 +220,9 @@ export const RecordInlineCellContainer = ({ isHovered={isHovered} emptyPlaceholder={showLabel ? 'Empty' : label} > - {displayModeContent} + {newDisplayModeContent} - {showEditButton && ( - - - - )} + {showEditButton && } )} diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx index 3bd50946bce3..0475a925606a 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx @@ -1,13 +1,23 @@ +import styled from '@emotion/styled'; import { IconComponent } from 'twenty-ui'; +import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer'; import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; +const StyledInlineCellButtonContainer = styled.div` + align-items: center; + display: flex; +`; export const RecordInlineCellButton = ({ Icon }: { Icon: IconComponent }) => { return ( - + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/AnimatedContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/AnimatedContainer.tsx new file mode 100644 index 000000000000..c821abb3794f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/components/AnimatedContainer.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { motion } from 'framer-motion'; + +const StyledAnimatedChipContainer = styled(motion.div)``; + +export const AnimatedContainer = ({ + children, +}: { + children: React.ReactNode; +}) => ( + + {children} + +); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx index 201043957a81..9d89372d7cd8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx @@ -102,7 +102,7 @@ export const RecordTableCell = ({ isReadOnly={isReadOnly} /> } - nonEditModeContent={} + nonEditModeContent={} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx index 51202803b217..287df8331f07 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx @@ -1,12 +1,11 @@ import styled from '@emotion/styled'; -import { motion } from 'framer-motion'; import { IconComponent } from 'twenty-ui'; +import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer'; import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; -const StyledEditButtonContainer = styled(motion.div)` - position: absolute; - right: 5px; +const StyledButtonContainer = styled.div` + margin: ${({ theme }) => theme.spacing(1)}; `; type RecordTableCellButtonProps = { @@ -18,12 +17,9 @@ export const RecordTableCellButton = ({ onClick, Icon, }: RecordTableCellButtonProps) => ( - - - + + + + + ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx index 044bf895513c..17cc3011af02 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx @@ -1,4 +1,10 @@ -import React, { ReactElement, useContext, useState } from 'react'; +import React, { + ReactElement, + useContext, + useEffect, + useRef, + useState, +} from 'react'; import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; import { IconArrowUpRight } from 'twenty-ui'; @@ -14,6 +20,7 @@ import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/rec import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext'; import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState'; import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState'; +import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId'; @@ -33,7 +40,7 @@ const StyledTd = styled.td<{ isSelected: boolean; isInEditMode: boolean }>` z-index: ${({ isInEditMode }) => (isInEditMode ? '4 !important' : '3')}; `; -const StyledCellBaseContainer = styled.div` +const StyledCellBaseContainer = styled.div<{ softFocus: boolean }>` align-items: center; box-sizing: border-box; cursor: pointer; @@ -41,6 +48,12 @@ const StyledCellBaseContainer = styled.div` height: 32px; position: relative; user-select: none; + ${(props) => + props.softFocus + ? `background: ${props.theme.background.transparent.secondary}; + border-radius: ${props.theme.border.radius.sm}; + outline: 1px solid ${props.theme.font.color.extraLight};` + : ''} `; export type RecordTableCellContainerProps = { @@ -63,6 +76,10 @@ export const RecordTableCellContainer = ({ editHotkeyScope, }: RecordTableCellContainerProps) => { const { columnIndex } = useContext(RecordTableCellContext); + const reference = useRef(null); + const [isHovered, setIsHovered] = useState(false); + const [newNonEditModeContent, setNewNonEditModeContent] = + useState(nonEditModeContent); const { isReadOnly, isSelected, recordId } = useContext( RecordTableRowContext, ); @@ -71,8 +88,6 @@ export const RecordTableCellContainer = ({ const cellPosition = useCurrentTableCellPosition(); - const [isHovered, setIsHovered] = useState(false); - const { openTableCell } = useOpenRecordTableCellFromCell(); const tableScopeId = useAvailableScopeIdOrThrow( @@ -135,8 +150,20 @@ export const RecordTableCellContainer = ({ (!isFirstColumn || !isEmpty) && !isReadOnly; + useEffect(() => { + if (React.isValidElement(nonEditModeContent)) { + setNewNonEditModeContent( + React.cloneElement(nonEditModeContent, { + isHovered: showButton, + reference: reference.current || undefined, + }), + ); + } + }, [nonEditModeContent, showButton, reference]); + return ( {isCurrentTableCellInEditMode ? ( {editModeContent} ) : hasSoftFocus ? ( <> + + {editModeContentOnly ? editModeContent : newNonEditModeContent} + {showButton && ( )} - - {editModeContentOnly ? editModeContent : nonEditModeContent} - ) : ( <> + {!isEmpty && ( + + {editModeContentOnly + ? editModeContent + : newNonEditModeContent} + + )} {showButton && ( )} - {!isEmpty && ( - - {editModeContentOnly ? editModeContent : nonEditModeContent} - - )} )} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditButton.tsx deleted file mode 100644 index 5aec39f9a1f7..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import styled from '@emotion/styled'; -import { motion } from 'framer-motion'; -import { IconComponent } from 'twenty-ui'; - -import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; - -const StyledEditButtonContainer = styled(motion.div)` - position: absolute; - right: 5px; -`; - -type RecordTableCellEditButtonProps = { - onClick?: () => void; - Icon: IconComponent; -}; - -export const RecordTableCellEditButton = ({ - onClick, - Icon, -}: RecordTableCellEditButtonProps) => ( - - - -); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx index ecec3005c8ef..3c016ddb2006 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx @@ -101,7 +101,6 @@ export const RecordTableCellSoftFocusMode = ({ return ( {children} diff --git a/packages/twenty-front/src/modules/ui/display/expandable-list/ExpandableList.tsx b/packages/twenty-front/src/modules/ui/display/expandable-list/ExpandableList.tsx deleted file mode 100644 index 631dd788bb95..000000000000 --- a/packages/twenty-front/src/modules/ui/display/expandable-list/ExpandableList.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { ReactElement, useRef, useState } from 'react'; -import { createPortal } from 'react-dom'; -import styled from '@emotion/styled'; -import { Chip, ChipVariant } from 'twenty-ui'; - -import { IntersectionObserverWrapper } from '@/ui/display/expandable-list/IntersectionObserverWrapper'; -import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; - -const StyledContainer = styled.div` - align-items: center; - display: flex; - flex: 1; - gap: ${({ theme }) => theme.spacing(1)}; - box-sizing: border-box; - white-space: nowrap; - overflow-x: hidden; -`; - -const StyledExpendableCell = styled.div` - align-content: center; - align-items: center; - backdrop-filter: ${({ theme }) => theme.blur.strong}; - background: ${({ theme }) => theme.background.secondary}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; - border-radius: ${({ theme }) => theme.border.radius.sm}; - box-shadow: ${({ theme }) => theme.boxShadow.light}; - box-sizing: border-box; - display: flex; - flex-wrap: wrap; - gap: ${({ theme }) => theme.spacing(1)}; - padding: ${({ theme }) => theme.spacing(2)}; -`; - -export const ExpandableList = ({ - listItems, - rootRef, - id, -}: { - listItems: ReactElement[]; - rootRef: React.RefObject; - id: string; -}) => { - const [listItemsInView, setListItemsInView] = useState(new Set()); - - const firstListItem = listItems[0]; - - const dropdownId = `expandable-list-dropdown-${id}`; - - const containerRef = useRef(null); - - const divRef = useRef(null); - - return ( - - {firstListItem} - {listItems.slice(1).map((listItem, index) => ( - - - {listItem} - - {index === listItemsInView.size - 1 && - listItems.length - listItemsInView.size - 1 !== 0 && ( - - } - dropdownComponents={ - <> - {divRef.current && - createPortal( - - {listItems} - , - divRef.current as HTMLElement, - )} - - } - /> - )} - - ))} -
-
- ); -}; diff --git a/packages/twenty-front/src/modules/ui/display/expandable-list/IntersectionObserverWrapper.tsx b/packages/twenty-front/src/modules/ui/display/expandable-list/IntersectionObserverWrapper.tsx deleted file mode 100644 index d4a81d2e2974..000000000000 --- a/packages/twenty-front/src/modules/ui/display/expandable-list/IntersectionObserverWrapper.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { useInView } from 'react-intersection-observer'; -import styled from '@emotion/styled'; - -const StyledDiv = styled.div<{ inView?: boolean }>` - opacity: ${({ inView }) => (inView === undefined || inView ? 1 : 0)}; -`; - -export const IntersectionObserverWrapper = ({ - set, - id, - rootRef, - children, -}: { - set: React.Dispatch>>; - id: number; - rootRef?: React.RefObject; - children: React.ReactNode; -}) => { - const { ref, inView } = useInView({ - threshold: 1, - onChange: (inView) => { - if (inView) { - set((prev: Set) => { - const newSet = new Set(prev); - newSet.add(id); - return newSet; - }); - } - if (!inView) { - set((prev: Set) => { - const newSet = new Set(prev); - newSet.delete(id); - return newSet; - }); - } - }, - root: rootRef?.current, - rootMargin: '0px 0px -50px 0px', - }); - - return ( - - {children} - - ); -}; diff --git a/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx b/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx index 22ab79bf128f..822c193e8d15 100644 --- a/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx +++ b/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx @@ -3,7 +3,7 @@ import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { IconInfoCircle } from 'twenty-ui'; -import { Button } from '@/ui/input/button/components/Button.tsx'; +import { Button } from '@/ui/input/button/components/Button'; export type InfoAccent = 'blue' | 'danger'; export type InfoProps = { diff --git a/packages/twenty-front/src/modules/ui/input/components/CardPicker.tsx b/packages/twenty-front/src/modules/ui/input/components/CardPicker.tsx index b0d42ad010d3..e80b990fe1c5 100644 --- a/packages/twenty-front/src/modules/ui/input/components/CardPicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/CardPicker.tsx @@ -1,7 +1,8 @@ import React from 'react'; import styled from '@emotion/styled'; -import { Radio } from '@/ui/input/components/Radio.tsx'; +import { Radio } from '@/ui/input/components/Radio'; + const StyledSubscriptionCardContainer = styled.button` background-color: ${({ theme }) => theme.background.secondary}; border: 1px solid ${({ theme }) => theme.border.color.medium}; diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ChildrenContainer.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ChildrenContainer.tsx new file mode 100644 index 000000000000..a22cff682c57 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ChildrenContainer.tsx @@ -0,0 +1,57 @@ +import { Dispatch, ReactElement, SetStateAction } from 'react'; +import styled from '@emotion/styled'; + +import { ChildrenProperty } from '@/ui/layout/expandable-list/components/ExpandableList'; + +const StyledChildContainer = styled.div<{ + shrink?: number; + isVisible?: boolean; + displayHiddenCount?: boolean; +}>` + display: ${({ isVisible = true }) => (isVisible ? 'flex' : 'none')}; + flex-shrink: ${({ shrink = 1 }) => shrink}; + overflow: ${({ displayHiddenCount }) => + displayHiddenCount ? 'hidden' : 'none'}; +`; + +const StyledChildrenContainer = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; + overflow: hidden; +`; +export const ChildrenContainer = ({ + children, + childrenProperties, + setChildrenWidths, + isFocusedMode, +}: { + children: ReactElement[]; + childrenProperties: Record; + setChildrenWidths: Dispatch>>; + isFocusedMode: boolean; +}) => { + return ( + + {children.map((child, index) => { + return ( + { + if (!el || isFocusedMode) return; + setChildrenWidths((prevState) => { + prevState[index] = el.getBoundingClientRect().width; + return prevState; + }); + }} + key={index} + displayHiddenCount={isFocusedMode} + isVisible={childrenProperties[index]?.isVisible} + shrink={childrenProperties[index]?.shrink} + > + {child} + + ); + })} + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx new file mode 100644 index 000000000000..6fe0f6d3de53 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx @@ -0,0 +1,147 @@ +import { ReactElement, useEffect, useState } from 'react'; +import styled from '@emotion/styled'; +import { offset, useFloating } from '@floating-ui/react'; +import { Chip, ChipVariant } from 'twenty-ui'; + +import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer'; +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { ChildrenContainer } from '@/ui/layout/expandable-list/components/ChildrenContainer'; +import { getChildrenProperties } from '@/ui/layout/expandable-list/utils/getChildProperties'; +import { getChipContentWidth } from '@/ui/layout/expandable-list/utils/getChipContentWidth'; + +export const GAP_WIDTH = 4; + +const StyledContainer = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; + justify-content: space-between; + width: 100%; +`; + +const StyledRelationsListContainer = styled.div<{ + withDropDownBorder?: boolean; +}>` + backdrop-filter: ${({ theme }) => theme.blur.strong}; + background-color: ${({ theme }) => theme.background.secondary}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + box-shadow: '0px 2px 4px ${({ theme }) => + theme.boxShadow.light}, 2px 4px 16px ${({ theme }) => + theme.boxShadow.strong}'; + display: flex; + flex-wrap: wrap; + gap: ${({ theme }) => theme.spacing(1)}; + padding: ${({ theme }) => theme.spacing(2)}; + outline: ${(props) => + props.withDropDownBorder + ? `1px solid ${props.theme.font.color.extraLight}` + : 'none'}; +`; + +export type ExpandableListProps = { + isHovered?: boolean; + reference?: HTMLDivElement; + forceDisplayHiddenCount?: boolean; + withDropDownBorder?: boolean; +}; + +export type ChildrenProperty = { + shrink: number; + isVisible: boolean; +}; + +export const ExpandableList = ({ + children, + isHovered, + reference, + forceDisplayHiddenCount = false, + withDropDownBorder = false, +}: { + children: ReactElement[]; +} & ExpandableListProps) => { + const [containerWidth, setContainerWidth] = useState(0); + const [isDropdownMenuOpen, setIsDropdownMenuOpen] = useState(false); + const [childrenWidths, setChildrenWidths] = useState>( + {}, + ); + + // Because Chip width depends on the number of hidden children which depends on the Chip width, we have a circular dependency + // To avoid it, we set the Chip width and make sure it can display its content (a number greater than 1) + const chipContentWidth = getChipContentWidth(children.length); + const chipContainerWidth = chipContentWidth + 2 * GAP_WIDTH; // Because Chip component has 4px padding-left and right + const availableWidth = containerWidth - (chipContainerWidth + GAP_WIDTH); // Because there is a 4px gap between ChildrenContainer and ChipContainer + const isFocusedMode = + (isHovered || forceDisplayHiddenCount) && + Object.values(childrenWidths).length > 0; + + const childrenProperties = getChildrenProperties( + isFocusedMode, + availableWidth, + childrenWidths, + ); + + const hiddenChildrenCount = Object.values(childrenProperties).filter( + (childProperties) => !childProperties.isVisible, + ).length; + + const displayHiddenCountChip = isFocusedMode && hiddenChildrenCount > 0; + + const { refs, floatingStyles } = useFloating({ + // @ts-expect-error placement accepts 'start' as value even if the typing does not permit it + placement: 'start', + middleware: [offset({ mainAxis: -1, crossAxis: -1 })], + elements: { reference }, + }); + + const openDropdownMenu = (event: React.MouseEvent) => { + event.stopPropagation(); + setIsDropdownMenuOpen(true); + }; + + useEffect(() => { + if (!isHovered) { + setIsDropdownMenuOpen(false); + } + }, [isHovered]); + + return ( + { + if (!el) return; + setContainerWidth(el.getBoundingClientRect().width); + }} + > + + {children} + + {displayHiddenCountChip && ( + + + + )} + {isDropdownMenuOpen && ( + + + {children} + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/__stories__/ExpandableList.stories.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/__stories__/ExpandableList.stories.tsx new file mode 100644 index 000000000000..7cf3bcb38522 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/__stories__/ExpandableList.stories.tsx @@ -0,0 +1,74 @@ +import { ReactElement, useRef, useState } from 'react'; +import styled from '@emotion/styled'; +import { Meta, StoryObj } from '@storybook/react'; +import { ComponentDecorator } from 'packages/twenty-ui'; + +import { Tag } from '@/ui/display/tag/components/Tag'; +import { + ExpandableList, + ExpandableListProps, +} from '@/ui/layout/expandable-list/components/ExpandableList'; +import { MAIN_COLOR_NAMES } from '@/ui/theme/constants/MainColorNames'; + +const StyledContainer = styled.div` + padding: ${({ theme }) => theme.spacing(1)}; + width: 300px; +`; + +type RenderProps = ExpandableListProps & { + children: ReactElement[]; +}; + +const Render = (args: RenderProps) => { + const [isHovered, setIsHovered] = useState(false); + const reference = useRef(null); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {args.children} + + + ); +}; + +const meta: Meta = { + title: 'UI/Layout/ExpandableList/ExpandableList', + component: ExpandableList, + decorators: [ComponentDecorator], + args: { + children: [ + , + , + , + , + , + , + , + ], + isHovered: undefined, + reference: undefined, + forceDisplayHiddenCount: false, + withDropDownBorder: false, + }, + argTypes: { + children: { control: false }, + isHovered: { control: false }, + reference: { control: false }, + }, + render: Render, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChildProperties.test.ts b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChildProperties.test.ts new file mode 100644 index 000000000000..56e75c67d7ef --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChildProperties.test.ts @@ -0,0 +1,41 @@ +import { getChildrenProperties } from '@/ui/layout/expandable-list/utils/getChildProperties'; + +describe('getChildrenProperties', () => { + it('should return default value when isFocused is False', () => { + const isFocused = false; + const availableWidth = 100; + expect(getChildrenProperties(isFocused, availableWidth, {})).toEqual({}); + + expect( + getChildrenProperties(isFocused, availableWidth, { 0: 40, 1: 40 }), + ).toEqual({}); + }); + + it('should return proper value when isFocused is True', () => { + const isFocused = true; + const availableWidth = 100; + expect(getChildrenProperties(isFocused, availableWidth, {})).toEqual({}); + + expect( + getChildrenProperties(isFocused, availableWidth, { 0: 40, 1: 40 }), + ).toEqual({ + 0: { shrink: 0, isVisible: true }, + 1: { shrink: 0, isVisible: true }, + }); + expect( + getChildrenProperties(isFocused, availableWidth, { + 0: 40, + 1: 40, + 2: 40, + 3: 40, + 4: 40, + }), + ).toEqual({ + 0: { shrink: 0, isVisible: true }, + 1: { shrink: 0, isVisible: true }, + 2: { shrink: 1, isVisible: true }, + 3: { shrink: 1, isVisible: false }, + 4: { shrink: 1, isVisible: false }, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChipContentWidth.test.ts b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChipContentWidth.test.ts new file mode 100644 index 000000000000..61345210750e --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChipContentWidth.test.ts @@ -0,0 +1,13 @@ +import { getChipContentWidth } from '@/ui/layout/expandable-list/utils/getChipContentWidth'; + +describe('getChipContentWidth', () => { + it('should return proper value', () => { + expect(getChipContentWidth(0)).toEqual(0); + expect(getChipContentWidth(1)).toEqual(0); + expect(getChipContentWidth(2)).toEqual(17); + expect(getChipContentWidth(20)).toEqual(25); + expect(getChipContentWidth(200)).toEqual(33); + expect(getChipContentWidth(2000)).toEqual(41); + expect(getChipContentWidth(20000)).toEqual(49); + }); +}); diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChildProperties.ts b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChildProperties.ts new file mode 100644 index 000000000000..1528bedfce2c --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChildProperties.ts @@ -0,0 +1,30 @@ +import { + ChildrenProperty, + GAP_WIDTH, +} from '@/ui/layout/expandable-list/components/ExpandableList'; + +export const getChildrenProperties = ( + isFocusedMode: boolean, + availableWidth: number, + childrenWidths: Record, +) => { + if (!isFocusedMode) { + return {}; + } + let cumulatedChildrenWidth = 0; + const result: Record = {}; + Object.values(childrenWidths).forEach((width, index) => { + // Because there is a 4px gap between children + const childWidth = width + GAP_WIDTH; + let shrink = 1; + let isVisible = true; + if (cumulatedChildrenWidth > availableWidth) { + isVisible = false; + } else if (cumulatedChildrenWidth + childWidth <= availableWidth) { + shrink = 0; + } + result[index] = { shrink, isVisible }; + cumulatedChildrenWidth += childWidth; + }); + return result; +}; diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChipContentWidth.ts b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChipContentWidth.ts new file mode 100644 index 000000000000..dcf50862f7bf --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChipContentWidth.ts @@ -0,0 +1,6 @@ +export const getChipContentWidth = (numberOfChildren: number) => { + if (numberOfChildren <= 1) { + return 0; + } + return 17 + 8 * Math.trunc(Math.log10(numberOfChildren)); +}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx index b5de9a806336..907d16dda6bd 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx @@ -6,6 +6,7 @@ import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; +import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted'; import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; @@ -46,6 +47,9 @@ export const RightDrawer = () => { ); const isRightDrawerExpanded = useRecoilValue(isRightDrawerExpandedState); + const [, setIsRightDrawerAnimationCompleted] = useRecoilState( + isRightDrawerAnimationCompletedState, + ); const rightDrawerPage = useRecoilValue(rightDrawerPageState); @@ -112,6 +116,9 @@ export const RightDrawer = () => { x: '100%', }, }; + const handleAnimationComplete = () => { + setIsRightDrawerAnimationCompleted(isRightDrawerOpen); + }; return ( { transition={{ duration: theme.animation.duration.normal, }} + onAnimationComplete={handleAnimationComplete} > {isRightDrawerOpen && } diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted.ts new file mode 100644 index 000000000000..3e1cb030a30c --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const isRightDrawerAnimationCompletedState = createState({ + key: 'isRightDrawerAnimationCompletedState', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/GithubVersionLink.tsx b/packages/twenty-front/src/modules/ui/navigation/link/components/GithubVersionLink.tsx index 57e86a6e67d4..0d2ed27331cc 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/GithubVersionLink.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/link/components/GithubVersionLink.tsx @@ -1,7 +1,7 @@ import { useTheme } from '@emotion/react'; import { IconBrandGithub } from 'twenty-ui'; -import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx'; +import { ActionLink } from '@/ui/navigation/link/components/ActionLink'; import packageJson from '../../../../../../package.json'; import { GITHUB_LINK } from '../constants/GithubLink'; diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ActionLink.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ActionLink.stories.tsx index 522c24c112bf..bb7dc0de6ad7 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ActionLink.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ActionLink.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from 'twenty-ui'; -import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx'; +import { ActionLink } from '@/ui/navigation/link/components/ActionLink'; const meta: Meta = { title: 'UI/navigation/link/ActionLink', diff --git a/packages/twenty-front/src/pages/auth/ChooseYourPlan.tsx b/packages/twenty-front/src/pages/auth/ChooseYourPlan.tsx index 9736eabb660c..882e39df9596 100644 --- a/packages/twenty-front/src/pages/auth/ChooseYourPlan.tsx +++ b/packages/twenty-front/src/pages/auth/ChooseYourPlan.tsx @@ -3,21 +3,21 @@ import styled from '@emotion/styled'; import { isNonEmptyString, isNumber } from '@sniptt/guards'; import { useRecoilValue } from 'recoil'; -import { SubTitle } from '@/auth/components/SubTitle.tsx'; -import { Title } from '@/auth/components/Title.tsx'; -import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit.tsx'; -import { SubscriptionCard } from '@/billing/components/SubscriptionCard.tsx'; +import { SubTitle } from '@/auth/components/SubTitle'; +import { Title } from '@/auth/components/Title'; +import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit'; +import { SubscriptionCard } from '@/billing/components/SubscriptionCard'; import { billingState } from '@/client-config/states/billingState'; import { AppPath } from '@/types/AppPath'; -import { Loader } from '@/ui/feedback/loader/components/Loader.tsx'; +import { Loader } from '@/ui/feedback/loader/components/Loader'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { MainButton } from '@/ui/input/button/components/MainButton.tsx'; -import { CardPicker } from '@/ui/input/components/CardPicker.tsx'; +import { MainButton } from '@/ui/input/button/components/MainButton'; +import { CardPicker } from '@/ui/input/components/CardPicker'; import { ProductPriceEntity, useCheckoutSessionMutation, useGetProductPricesQuery, -} from '~/generated/graphql.tsx'; +} from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; const StyledChoosePlanContainer = styled.div` diff --git a/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx b/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx index 81c93bcfee1c..5d598430b538 100644 --- a/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx +++ b/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx @@ -17,7 +17,7 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetada import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader'; import { AppPath } from '@/types/AppPath'; import { H2Title } from '@/ui/display/typography/components/H2Title'; -import { Loader } from '@/ui/feedback/loader/components/Loader.tsx'; +import { Loader } from '@/ui/feedback/loader/components/Loader'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; diff --git a/packages/twenty-front/src/pages/auth/PaymentSuccess.tsx b/packages/twenty-front/src/pages/auth/PaymentSuccess.tsx index 200a589b52d6..6be845d67338 100644 --- a/packages/twenty-front/src/pages/auth/PaymentSuccess.tsx +++ b/packages/twenty-front/src/pages/auth/PaymentSuccess.tsx @@ -4,12 +4,12 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { IconCheck } from 'twenty-ui'; -import { SubTitle } from '@/auth/components/SubTitle.tsx'; -import { Title } from '@/auth/components/Title.tsx'; +import { SubTitle } from '@/auth/components/SubTitle'; +import { Title } from '@/auth/components/Title'; import { AppPath } from '@/types/AppPath'; -import { MainButton } from '@/ui/input/button/components/MainButton.tsx'; +import { MainButton } from '@/ui/input/button/components/MainButton'; import { RGBA } from '@/ui/theme/constants/Rgba'; -import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn.tsx'; +import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn'; const StyledCheckContainer = styled.div` align-items: center; diff --git a/packages/twenty-front/tsconfig.json b/packages/twenty-front/tsconfig.json index 9b8b10f5230c..e55b6b262faa 100644 --- a/packages/twenty-front/tsconfig.json +++ b/packages/twenty-front/tsconfig.json @@ -9,7 +9,7 @@ "esModuleInterop": true, "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, - "allowImportingTsExtensions": true, + "allowImportingTsExtensions": false, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, diff --git a/packages/twenty-front/tsup.ui.index.tsx b/packages/twenty-front/tsup.ui.index.tsx index e16dafe3b1f0..ce31aa7a3c91 100644 --- a/packages/twenty-front/tsup.ui.index.tsx +++ b/packages/twenty-front/tsup.ui.index.tsx @@ -1,4 +1,4 @@ -import { ThemeType } from './src/modules/ui/theme/constants/ThemeLight'; +import { ThemeType } from '@/ui/theme/constants/ThemeLight'; export { ThemeProvider } from '@emotion/react'; @@ -21,7 +21,7 @@ export * from './src/modules/ui/input/button/components/FloatingButtonGroup' export * from './src/modules/ui/input/button/components/FloatingIconButton' export * from './src/modules/ui/input/button/components/FloatingIconButtonGroup' export * from './src/modules/ui/input/button/components/LightButton' -export * from './src/modules/ui/navigation/link/components/ActionLink.tsx' +export * from './src/modules/ui/navigation/link/components/ActionLink' export * from './src/modules/ui/input/button/components/LightIconButton' export * from './src/modules/ui/input/button/components/MainButton' export * from './src/modules/ui/input/button/components/RoundedIconButton' diff --git a/packages/twenty-ui/src/display/chip/components/Chip.tsx b/packages/twenty-ui/src/display/chip/components/Chip.tsx index 7f8130ae6a2d..4ddb950baad9 100644 --- a/packages/twenty-ui/src/display/chip/components/Chip.tsx +++ b/packages/twenty-ui/src/display/chip/components/Chip.tsx @@ -51,6 +51,8 @@ const StyledContainer = styled.div< cursor: ${({ clickable, disabled }) => clickable ? 'pointer' : disabled ? 'not-allowed' : 'inherit'}; display: inline-flex; + flex-direction: row-reverse; + justify-content: center; gap: ${({ theme }) => theme.spacing(1)}; height: ${({ theme }) => theme.spacing(3)}; max-width: ${({ maxWidth }) =>