diff --git a/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidget.tsx b/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidget.tsx index 809695bd414..0705dbf5e81 100644 --- a/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidget.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidget.tsx @@ -1,6 +1,14 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { RecordTableWidgetSetReadOnlyColumnHeadersEffect } from '@/object-record/record-table-widget/components/RecordTableWidgetSetReadOnlyColumnHeadersEffect'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; +import { styled } from '@linaria/react'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +const StyledTableContainer = styled.div` + border: 1px solid ${themeCssVariables.border.color.light}; + border-radius: ${themeCssVariables.border.radius.sm}; + overflow: hidden; +`; export const RecordTableWidget = () => { const { objectNameSingular, recordIndexId, viewBarInstanceId } = @@ -11,11 +19,13 @@ export const RecordTableWidget = () => { - + + + ); }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutRecordTableWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutRecordTableWidget.ts index 7bd75516b83..594186963ad 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutRecordTableWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutRecordTableWidget.ts @@ -1,3 +1,4 @@ +import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { WIDGET_SIZES } from '@/page-layout/constants/WidgetSizes'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; @@ -49,8 +50,10 @@ export const useCreatePageLayoutRecordTableWidget = ( const store = useStore(); - const createPageLayoutRecordTableWidget = - useCallback((): PageLayoutWidget => { + const createPageLayoutRecordTableWidget = useCallback( + ( + objectMetadata?: Pick, + ): PageLayoutWidget => { const allTabLayouts = store.get(pageLayoutCurrentLayoutsState); const pageLayoutDraggedArea = store.get(pageLayoutDraggedAreaState); @@ -70,17 +73,18 @@ export const useCreatePageLayoutRecordTableWidget = ( minimumSize, ); - const newWidget = createDefaultRecordTableWidget( - widgetId, - activeTabId, - 'Record Table', - { + const newWidget = createDefaultRecordTableWidget({ + id: widgetId, + pageLayoutTabId: activeTabId, + title: objectMetadata?.labelPlural ?? 'Record Table', + gridPosition: { row: position.y, column: position.x, rowSpan: position.h, columnSpan: position.w, }, - ); + objectMetadataId: objectMetadata?.id, + }); const newLayout = { i: widgetId, @@ -108,13 +112,15 @@ export const useCreatePageLayoutRecordTableWidget = ( store.set(pageLayoutDraggedAreaState, null); return newWidget; - }, [ + }, + [ activeTabId, pageLayoutCurrentLayoutsState, pageLayoutDraftState, pageLayoutDraggedAreaState, store, - ]); + ], + ); return { createPageLayoutRecordTableWidget }; }; diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultRecordTableWidget.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultRecordTableWidget.test.ts index 08983451cdf..caf3b6b34ab 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultRecordTableWidget.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultRecordTableWidget.test.ts @@ -1,9 +1,9 @@ +import { createDefaultRecordTableWidget } from '@/page-layout/utils/createDefaultRecordTableWidget'; import { PageLayoutTabLayoutMode, WidgetConfigurationType, WidgetType, } from '~/generated-metadata/graphql'; -import { createDefaultRecordTableWidget } from '@/page-layout/utils/createDefaultRecordTableWidget'; describe('createDefaultRecordTableWidget', () => { const widgetId = 'widget-id-1'; @@ -11,12 +11,12 @@ describe('createDefaultRecordTableWidget', () => { const title = 'My Record Table'; const gridPosition = { row: 1, column: 2, rowSpan: 3, columnSpan: 4 }; - const widget = createDefaultRecordTableWidget( - widgetId, + const widget = createDefaultRecordTableWidget({ + id: widgetId, pageLayoutTabId, title, gridPosition, - ); + }); it('should return correct shape with all required fields', () => { expect(widget.__typename).toBe('PageLayoutWidget'); diff --git a/packages/twenty-front/src/modules/page-layout/utils/createDefaultRecordTableWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/createDefaultRecordTableWidget.ts index 50d3edc9ff2..873632195b9 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/createDefaultRecordTableWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/createDefaultRecordTableWidget.ts @@ -6,12 +6,19 @@ import { WidgetType, } from '~/generated-metadata/graphql'; -export const createDefaultRecordTableWidget = ( - id: string, - pageLayoutTabId: string, - title: string, - gridPosition: GridPosition, -): PageLayoutWidget => { +export const createDefaultRecordTableWidget = ({ + id, + pageLayoutTabId, + title, + gridPosition, + objectMetadataId, +}: { + id: string; + pageLayoutTabId: string; + title: string; + gridPosition: GridPosition; + objectMetadataId?: string; +}): PageLayoutWidget => { return { __typename: 'PageLayoutWidget', id, @@ -30,7 +37,8 @@ export const createDefaultRecordTableWidget = ( rowSpan: gridPosition.rowSpan, columnSpan: gridPosition.columnSpan, }, - objectMetadataId: null, + objectMetadataId: objectMetadataId ?? null, + isOverridden: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), deletedAt: null, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget.ts index 28bbfc471b5..ea090a53b87 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget.ts @@ -1,12 +1,15 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { filterFieldsForRecordTableViewCreation } from '@/page-layout/widgets/record-table/utils/filterFieldsForRecordTableViewCreation'; import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget'; -import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig'; +import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist'; import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist'; import { useCallback } from 'react'; import { v4 } from 'uuid'; -import { ViewType } from '~/generated-metadata/graphql'; +import { + WidgetConfigurationType, + ViewType, +} from '~/generated-metadata/graphql'; const DEFAULT_VIEW_FIELD_SIZE = 180; const INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET = 6; @@ -14,11 +17,13 @@ const INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET = 6; export const useCreateViewForRecordTableWidget = (pageLayoutId: string) => { const { performViewAPICreate } = usePerformViewAPIPersist(); const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist(); - const { updateCurrentWidgetConfig } = - useUpdateCurrentWidgetConfig(pageLayoutId); + const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId); const createViewForRecordTableWidget = useCallback( - async (objectMetadataItem: EnrichedObjectMetadataItem) => { + async ( + widgetId: string, + objectMetadataItem: EnrichedObjectMetadataItem, + ) => { const newViewId = v4(); const viewResult = await performViewAPICreate( @@ -60,8 +65,9 @@ export const useCreateViewForRecordTableWidget = (pageLayoutId: string) => { try { await performViewFieldAPICreate({ inputs: viewFieldInputs }); - updateCurrentWidgetConfig({ - configToUpdate: { + updatePageLayoutWidget(widgetId, { + configuration: { + configurationType: WidgetConfigurationType.RECORD_TABLE, viewId: newViewId, }, }); @@ -72,11 +78,7 @@ export const useCreateViewForRecordTableWidget = (pageLayoutId: string) => { ); } }, - [ - performViewAPICreate, - performViewFieldAPICreate, - updateCurrentWidgetConfig, - ], + [performViewAPICreate, performViewFieldAPICreate, updatePageLayoutWidget], ); return { createViewForRecordTableWidget }; diff --git a/packages/twenty-front/src/modules/side-panel/components/hooks/usePageLayoutHeaderInfo.ts b/packages/twenty-front/src/modules/side-panel/components/hooks/usePageLayoutHeaderInfo.ts index 71c65983d94..7596f5c5196 100644 --- a/packages/twenty-front/src/modules/side-panel/components/hooks/usePageLayoutHeaderInfo.ts +++ b/packages/twenty-front/src/modules/side-panel/components/hooks/usePageLayoutHeaderInfo.ts @@ -227,7 +227,7 @@ export const usePageLayoutHeaderInfo = ({ return { headerIcon: IconTable, headerIconColor: iconColor, - headerType: t`Record Table`, + headerType: t`View`, title, isReadonly: false, tab: undefined, diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutDashboardWidgetTypeSelect.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutDashboardWidgetTypeSelect.tsx index 118a63e1c5d..e5219c7bcef 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutDashboardWidgetTypeSelect.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutDashboardWidgetTypeSelect.tsx @@ -1,5 +1,6 @@ import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; import { FIND_MANY_FRONT_COMPONENTS } from '@/front-components/graphql/queries/findManyFrontComponents'; +import { useReadableObjectMetadataItems } from '@/object-metadata/hooks/useReadableObjectMetadataItems'; import { useCreatePageLayoutFrontComponentWidget } from '@/page-layout/hooks/useCreatePageLayoutFrontComponentWidget'; import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget'; import { useCreatePageLayoutIframeWidget } from '@/page-layout/hooks/useCreatePageLayoutIframeWidget'; @@ -10,6 +11,7 @@ import { useRemovePageLayoutWidgetAndPreservePosition } from '@/page-layout/hook import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord'; +import { useCreateViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget'; import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; import { SidePanelList } from '@/side-panel/components/SidePanelList'; import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; @@ -23,7 +25,7 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useQuery } from '@apollo/client/react'; import { t } from '@lingui/core/macro'; -import { SidePanelPages } from 'twenty-shared/types'; +import { CoreObjectNameSingular, SidePanelPages } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { IconAlignBoxLeftTop, @@ -86,6 +88,19 @@ export const SidePanelPageLayoutDashboardWidgetTypeSelect = () => { const { removePageLayoutWidgetAndPreservePosition } = useRemovePageLayoutWidgetAndPreservePosition(pageLayoutId); + const { createViewForRecordTableWidget } = + useCreateViewForRecordTableWidget(pageLayoutId); + const { readableObjectMetadataItems } = useReadableObjectMetadataItems(); + + const firstAvailableObjectMetadataItem = + readableObjectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === CoreObjectNameSingular.Company, + ) || + [...readableObjectMetadataItems].sort((first, second) => + first.labelPlural.localeCompare(second.labelPlural), + )[0]; + const isRecordTableWidgetEnabled = useIsFeatureEnabled( FeatureFlagKey.IS_RECORD_TABLE_WIDGET_ENABLED, ); @@ -179,7 +194,7 @@ export const SidePanelPageLayoutDashboardWidgetTypeSelect = () => { closeSidePanelMenu(); }; - const handleNavigateToRecordTableSettings = () => { + const handleNavigateToRecordTableSettings = async () => { if ( isExistingWidgetMissingOrDifferentType( existingWidget?.type, @@ -190,9 +205,16 @@ export const SidePanelPageLayoutDashboardWidgetTypeSelect = () => { removePageLayoutWidgetAndPreservePosition(pageLayoutEditingWidgetId); } - const newRecordTableWidget = createPageLayoutRecordTableWidget(); + const newRecordTableWidget = createPageLayoutRecordTableWidget( + firstAvailableObjectMetadataItem, + ); setPageLayoutEditingWidgetId(newRecordTableWidget.id); + + await createViewForRecordTableWidget( + newRecordTableWidget.id, + firstAvailableObjectMetadataItem, + ); } navigatePageLayoutSidePanel({ @@ -251,7 +273,7 @@ export const SidePanelPageLayoutDashboardWidgetTypeSelect = () => { > diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dashboard/SidePanelDashboardRecordTableSettings.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dashboard/SidePanelDashboardRecordTableSettings.tsx index 8ad8c80dc56..29c8fcaa205 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dashboard/SidePanelDashboardRecordTableSettings.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dashboard/SidePanelDashboardRecordTableSettings.tsx @@ -8,6 +8,7 @@ import { RecordTableDataSourceDropdownContent } from '@/side-panel/pages/page-la import { RecordTableFieldsDropdownContent } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableFieldsDropdownContent'; import { WidgetSettingsFooter } from '@/side-panel/pages/page-layout/components/WidgetSettingsFooter'; import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore'; +import { useRecordTableSettingsDescriptions } from '@/side-panel/pages/page-layout/hooks/useRecordTableSettingsDescriptions'; import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode'; import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; @@ -17,9 +18,10 @@ import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; import { IconArrowsSort, - IconDatabase, - IconEye, + IconBox, IconFilter, + IconListDetails, + IconTable, } from 'twenty-ui/display'; import { WidgetConfigurationType } from '~/generated-metadata/graphql'; @@ -41,21 +43,31 @@ export const SidePanelDashboardRecordTableSettings = () => { const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId); const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory(); - if (!isDefined(widgetInEditMode)) { - return null; - } - - const { configuration } = widgetInEditMode; - + const configuration = widgetInEditMode?.configuration; const isRecordTableConfiguration = - configuration.configurationType === WidgetConfigurationType.RECORD_TABLE; + configuration?.configurationType === WidgetConfigurationType.RECORD_TABLE; const viewId = isRecordTableConfiguration && + isDefined(configuration) && 'viewId' in configuration && isDefined(configuration.viewId) ? (configuration.viewId as string) - : undefined; + : null; + + const { + sourceDescription, + fieldsDescription, + filterDescription, + sortDescription, + } = useRecordTableSettingsDescriptions({ + objectMetadataId: widgetInEditMode?.objectMetadataId, + viewId, + }); + + if (!isDefined(widgetInEditMode)) { + return null; + } const hasViewId = isDefined(viewId); @@ -84,10 +96,23 @@ export const SidePanelDashboardRecordTableSettings = () => { commandGroups={[]} selectableItemIds={selectableItemIds} > - + + + } + dropdownPlacement="bottom-end" + description={t`Table`} + disabled={true} + contextualTextPosition="right" + /> + { } dropdownPlacement="bottom-end" hasSubMenu + description={sourceDescription} contextualTextPosition="right" /> @@ -105,7 +131,7 @@ export const SidePanelDashboardRecordTableSettings = () => { <> { } dropdownPlacement="bottom-end" hasSubMenu + description={fieldsDescription} contextualTextPosition="right" /> @@ -130,6 +157,7 @@ export const SidePanelDashboardRecordTableSettings = () => { Icon={IconFilter} hasSubMenu onClick={handleFilterClick} + description={filterDescription} contextualTextPosition="right" /> @@ -143,6 +171,7 @@ export const SidePanelDashboardRecordTableSettings = () => { Icon={IconArrowsSort} hasSubMenu onClick={handleSortClick} + description={sortDescription} contextualTextPosition="right" /> diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableDataSourceDropdownContent.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableDataSourceDropdownContent.tsx index 137aac59d15..892fc21c640 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableDataSourceDropdownContent.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableDataSourceDropdownContent.tsx @@ -105,14 +105,15 @@ export const RecordTableDataSourceDropdownContent = () => { (item) => item.id === newObjectMetadataItemId, ); - if (isDefined(selectedObjectMetadataItem)) { - await createViewForRecordTableWidget(selectedObjectMetadataItem); + if (isDefined(selectedObjectMetadataItem) && isDefined(widgetInEditMode)) { + await createViewForRecordTableWidget( + widgetInEditMode.id, + selectedObjectMetadataItem, + ); - if (isDefined(widgetInEditMode)) { - updatePageLayoutWidget(widgetInEditMode.id, { - title: selectedObjectMetadataItem.labelPlural, - }); - } + updatePageLayoutWidget(widgetInEditMode.id, { + title: selectedObjectMetadataItem.labelPlural, + }); } closeDropdown(); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/hooks/useRecordTableSettingsDescriptions.ts b/packages/twenty-front/src/modules/side-panel/pages/page-layout/hooks/useRecordTableSettingsDescriptions.ts new file mode 100644 index 00000000000..0bc6450c824 --- /dev/null +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/hooks/useRecordTableSettingsDescriptions.ts @@ -0,0 +1,114 @@ +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useViewById } from '@/views/hooks/useViewById'; +import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; + +const PLACEHOLDER_INSTANCE_ID = + 'record-table-settings-descriptions-placeholder'; + +type UseRecordTableSettingsDescriptionsParams = { + objectMetadataId: string | null | undefined; + viewId: string | null; +}; + +export const useRecordTableSettingsDescriptions = ({ + objectMetadataId, + viewId, +}: UseRecordTableSettingsDescriptionsParams) => { + const { objectMetadataItems } = useObjectMetadataItems(); + const { view } = useViewById(viewId); + + const sourceObjectMetadataItem = objectMetadataItems.find( + (item) => item.id === objectMetadataId, + ); + + const recordIndexId = + isDefined(viewId) && isDefined(sourceObjectMetadataItem) + ? getRecordIndexIdFromObjectNamePluralAndViewId( + sourceObjectMetadataItem.namePlural, + viewId, + ) + : PLACEHOLDER_INSTANCE_ID; + + const currentRecordFilters = useAtomComponentStateValue( + currentRecordFiltersComponentState, + recordIndexId, + ); + + const currentRecordSorts = useAtomComponentStateValue( + currentRecordSortsComponentState, + recordIndexId, + ); + + const currentRecordFields = useAtomComponentStateValue( + currentRecordFieldsComponentState, + recordIndexId, + ); + + const hasViewId = isDefined(viewId); + + const activeVisibleFieldLabels: Array = hasViewId + ? currentRecordFields + .filter((field) => field.isVisible) + .map( + (field) => + sourceObjectMetadataItem?.fields.find( + (metaField) => metaField.id === field.fieldMetadataItemId, + )?.label, + ) + : []; + + const activeFilterLabels: Array = + currentRecordFilters.length > 0 + ? currentRecordFilters.map((filter) => filter.label) + : (view?.viewFilters ?? []).map( + (filter) => + sourceObjectMetadataItem?.fields.find( + (field) => field.id === filter.fieldMetadataId, + )?.label, + ); + + const activeSortLabels: Array = ( + currentRecordSorts.length > 0 ? currentRecordSorts : (view?.viewSorts ?? []) + ).map( + (sort) => + sourceObjectMetadataItem?.fields.find( + (field) => field.id === sort.fieldMetadataId, + )?.label, + ); + + const sourceDescription = sourceObjectMetadataItem?.labelPlural; + + const fieldsDescription = + activeVisibleFieldLabels.length === 1 + ? activeVisibleFieldLabels[0] + : activeVisibleFieldLabels.length > 1 + ? t`${activeVisibleFieldLabels.length} shown` + : undefined; + + const filterDescription = + activeFilterLabels.length === 1 + ? activeFilterLabels[0] + : activeFilterLabels.length > 1 + ? t`${activeFilterLabels.length} shown` + : undefined; + + const sortDescription = + activeSortLabels.length === 1 + ? activeSortLabels[0] + : activeSortLabels.length > 1 + ? t`${activeSortLabels.length} shown` + : undefined; + + return { + sourceDescription, + fieldsDescription, + filterDescription, + sortDescription, + }; +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.tsx index 5d62e31e141..e7530f46cea 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.tsx @@ -135,12 +135,13 @@ export const MenuItem = ({ )} {RightComponent} - {hasSubMenu && !disabled && ( + {hasSubMenu && ( (null); const innerRef = useRef(null); - const headlineRef = useRef(null); const cardRefs = useRef<(HTMLDivElement | null)[]>([]); return ( - + - + diff --git a/packages/twenty-website-new/src/sections/Helped/effect-components/HelpedSceneScrollLayoutEffect.tsx b/packages/twenty-website-new/src/sections/Helped/effect-components/HelpedSceneScrollLayoutEffect.tsx index d831887ec64..79c33dd18e6 100644 --- a/packages/twenty-website-new/src/sections/Helped/effect-components/HelpedSceneScrollLayoutEffect.tsx +++ b/packages/twenty-website-new/src/sections/Helped/effect-components/HelpedSceneScrollLayoutEffect.tsx @@ -15,14 +15,12 @@ type HelpedSceneScrollLayoutEffectProps = HelpedSceneLayoutRefs & { export function HelpedSceneScrollLayoutEffect({ cardRefs, cards, - headlineRef, innerRef, sectionRef, }: HelpedSceneScrollLayoutEffectProps) { useEffect(() => { const refs: HelpedSceneLayoutRefs = { cardRefs, - headlineRef, innerRef, sectionRef, }; @@ -51,7 +49,7 @@ export function HelpedSceneScrollLayoutEffect({ window.cancelAnimationFrame(rafId); } }; - }, [cardRefs, cards, headlineRef, innerRef, sectionRef]); + }, [cardRefs, cards, innerRef, sectionRef]); return null; } diff --git a/packages/twenty-website-new/src/sections/Helped/utils/helped-scene-layout.ts b/packages/twenty-website-new/src/sections/Helped/utils/helped-scene-layout.ts index eb5aaa5f051..f605f5e70b5 100644 --- a/packages/twenty-website-new/src/sections/Helped/utils/helped-scene-layout.ts +++ b/packages/twenty-website-new/src/sections/Helped/utils/helped-scene-layout.ts @@ -3,60 +3,36 @@ import type { RefObject } from 'react'; import type { HeadingCardType } from '@/sections/Helped/types/HeadingCard'; import { theme } from '@/theme'; -export const CARD_WIDTH_DESKTOP = 443; -const CARD_WIDTH_MOBILE_MAX = 360; -const HORIZONTAL_PAD = 32; - -// Scroll progress 0..1 through the tall section -const HEADLINE_FADE_START = 0.76; -const HEADLINE_FADE_END = 0.92; -const HEADLINE_LIFT_PX = 48; - -// Each card fades/slides in over the same window, shifted along the scroll -const CARD_WINDOW = 0.2; -const CARD_STAGGER = 0.12; -const CARD_FIRST_START = 0.06; -const ENTER_OFFSET_Y_RATIO = 0.34; +const CARD_WIDTH_DESKTOP = 443; +const CARD_WIDTH_MOBILE = 360; +const PROGRESS_SCALE = 1.25; +const FADE_FRACTION = 0.15; function clamp01(value: number) { return Math.min(1, Math.max(0, value)); } -function cardRestPosition( +// Right, left, center for cards 0, 1, 2+ +function cardLeft( index: number, innerWidth: number, - innerHeight: number, cardWidth: number, isDesktop: boolean, -): { left: number; top: number } { - if (isDesktop) { - if (index === 0) { - return { - left: innerWidth - cardWidth - innerWidth * 0.035, - top: innerHeight * 0.08, - }; - } - if (index === 1) { - return { - left: innerWidth * 0.03, - top: innerHeight * 0.3, - }; - } - return { - left: (innerWidth - cardWidth) / 2, - top: innerHeight * 0.52, - }; +): number { + if (!isDesktop) { + return (innerWidth - cardWidth) / 2; } - - return { - left: (innerWidth - cardWidth) / 2, - top: innerHeight * 0.36 + index * 0.06 * innerHeight, - }; + if (index === 0) { + return innerWidth - cardWidth - innerWidth * 0.035; + } + if (index === 1) { + return innerWidth * 0.03; + } + return (innerWidth - cardWidth) / 2; } export type HelpedSceneLayoutRefs = { cardRefs: RefObject<(HTMLDivElement | null)[]>; - headlineRef: RefObject; innerRef: RefObject; sectionRef: RefObject; }; @@ -65,9 +41,8 @@ export function applyHelpedSceneLayout( refs: HelpedSceneLayoutRefs, cards: readonly HeadingCardType[], ): void { - const { sectionRef, innerRef, headlineRef, cardRefs } = refs; - const section = sectionRef.current; - const inner = innerRef.current; + const section = refs.sectionRef.current; + const inner = refs.innerRef.current; if (!section || !inner) { return; } @@ -79,61 +54,43 @@ export function applyHelpedSceneLayout( `(min-width: ${theme.breakpoints.md}px)`, ).matches; - const rect = section.getBoundingClientRect(); const scrollRange = Math.max(1, section.offsetHeight - window.innerHeight); - const progress = clamp01(-rect.top / scrollRange); + const progress = + clamp01(-section.getBoundingClientRect().top / scrollRange) * PROGRESS_SCALE; const innerWidth = inner.offsetWidth; const innerHeight = inner.offsetHeight; const cardWidth = Math.min( - isDesktop ? CARD_WIDTH_DESKTOP : CARD_WIDTH_MOBILE_MAX, - innerWidth - HORIZONTAL_PAD * 2, + isDesktop ? CARD_WIDTH_DESKTOP : CARD_WIDTH_MOBILE, + innerWidth - 64, ); - if (headlineRef.current) { - const headlineT = clamp01( - (progress - HEADLINE_FADE_START) / - (HEADLINE_FADE_END - HEADLINE_FADE_START), - ); - headlineRef.current.style.opacity = String(1 - headlineT); - headlineRef.current.style.transform = `translate3d(0, ${-headlineT * HEADLINE_LIFT_PX}px, 0)`; - } - - const enterOffsetY = innerHeight * ENTER_OFFSET_Y_RATIO; - cards.forEach((_, index) => { - const node = cardRefs.current[index]; + const node = refs.cardRefs.current[index]; if (!node) { return; } - const { left: restLeft, top: restTop } = cardRestPosition( - index, - innerWidth, - innerHeight, - cardWidth, - isDesktop, - ); - node.style.width = `${cardWidth}px`; node.style.zIndex = String(10 + index); + node.style.left = `${cardLeft(index, innerWidth, cardWidth, isDesktop)}px`; if (reducedMotion) { node.style.opacity = '1'; - node.style.transform = 'none'; - node.style.left = `${restLeft}px`; - node.style.top = `${restTop}px`; + node.style.top = `${innerHeight * (0.15 + index * 0.25)}px`; return; } - const windowStart = CARD_FIRST_START + index * CARD_STAGGER; - const windowEnd = windowStart + CARD_WINDOW; - const cardT = clamp01( - (progress - windowStart) / (windowEnd - windowStart), - ); + // Card 1: 0.05–0.55, Card 2: 0.35–0.85, Card 3: 0.65–1.15 + const cardStart = 0.05 + index * 0.3; + const travel = clamp01((progress - cardStart) / 0.5); - node.style.left = `${restLeft}px`; - node.style.top = `${restTop + enterOffsetY * (1 - cardT)}px`; - node.style.opacity = String(cardT); + node.style.top = `${innerHeight * (1.1 - travel * 1.4)}px`; + node.style.opacity = String( + Math.min( + clamp01(travel / FADE_FRACTION), + clamp01((1 - travel) / FADE_FRACTION), + ), + ); }); } diff --git a/packages/twenty-website-new/src/sections/HomeStepper/components/Flow/Flow.tsx b/packages/twenty-website-new/src/sections/HomeStepper/components/Flow/Flow.tsx index 73fd377e2cd..e32989c9d60 100644 --- a/packages/twenty-website-new/src/sections/HomeStepper/components/Flow/Flow.tsx +++ b/packages/twenty-website-new/src/sections/HomeStepper/components/Flow/Flow.tsx @@ -33,7 +33,7 @@ export function Flow({ scrollContainerRef, steps }: FlowProps) { steps={steps} /> - + ); diff --git a/packages/twenty-website-new/src/sections/HomeStepper/components/LeftColumn/LeftColumn.tsx b/packages/twenty-website-new/src/sections/HomeStepper/components/LeftColumn/LeftColumn.tsx index 6bc8bcb24cb..b82738ae542 100644 --- a/packages/twenty-website-new/src/sections/HomeStepper/components/LeftColumn/LeftColumn.tsx +++ b/packages/twenty-website-new/src/sections/HomeStepper/components/LeftColumn/LeftColumn.tsx @@ -1,30 +1,61 @@ 'use client'; import { Body, Heading } from '@/design-system/components'; +import { HOME_STEPPER_HOLD_FRACTIONS } from '@/sections/HomeStepper/utils/home-stepper-lottie-frame-map'; import { theme } from '@/theme'; import { styled } from '@linaria/react'; +import type { CSSProperties } from 'react'; import type { HomeStepperStepType } from '../../types/HomeStepperStep'; import { ProgressBar } from '../ProgressBar/ProgressBar'; const LeftColumnRoot = styled.div` - display: grid; - grid-template-columns: auto 1fr; - gap: ${theme.spacing(6)}; min-width: 0; @media (min-width: ${theme.breakpoints.md}px) { - gap: ${theme.spacing(20)}; + display: grid; margin-left: calc(-1 * ${theme.spacing(4)}); } `; -const StepsColumn = styled.div` +const StickyPanel = styled.div` + display: grid; + gap: ${theme.spacing(6)}; + grid-template-columns: auto 1fr; + + @media (min-width: ${theme.breakpoints.md}px) { + align-items: center; + align-self: start; + gap: ${theme.spacing(20)}; + grid-area: 1 / 1; + height: 100vh; + overflow: hidden; + position: sticky; + top: 0; + } +`; + +const SpacerStack = styled.div` + display: none; + + @media (min-width: ${theme.breakpoints.md}px) { + display: block; + grid-area: 1 / 1; + } +`; + +const StepSpacer = styled.div` + min-height: 100vh; +`; + +const StepsContainer = styled.div` display: grid; grid-template-columns: 1fr; min-width: 0; @media (min-width: ${theme.breakpoints.md}px) { - max-width: 454px; + & > * { + grid-area: 1 / 1; + } } `; @@ -32,18 +63,28 @@ const StepBlock = styled.div` align-content: center; display: grid; grid-template-columns: 1fr; + max-width: 454px; min-height: 80vh; + min-width: 0; opacity: 0.3; row-gap: ${theme.spacing(4)}; - - @media (min-width: ${theme.breakpoints.md}px) { - min-height: 100vh; - row-gap: ${theme.spacing(6)}; - } + transition: opacity 0.3s ease; &[data-active='true'] { opacity: 1; } + + @media (min-width: ${theme.breakpoints.md}px) { + min-height: 0; + row-gap: ${theme.spacing(6)}; + transition: none; + + &, + &[data-active='true'] { + opacity: var(--step-opacity, 0); + transform: var(--step-transform, translateY(40vh)); + } + } `; export type HomeStepperLeftColumnProps = { @@ -52,6 +93,36 @@ export type HomeStepperLeftColumnProps = { steps: HomeStepperStepType[]; }; +function computeDesktopStepStyle( + index: number, + activeStepIndex: number, + localProgress: number, + stepCount: number, +): { opacity: number; transform: string } { + const holdMax = HOME_STEPPER_HOLD_FRACTIONS[activeStepIndex] ?? 1; + + if (index === activeStepIndex) { + if (index === stepCount - 1 || localProgress <= holdMax) { + return { opacity: 1, transform: 'translateY(0)' }; + } + const t = Math.min(1, (localProgress - holdMax) / (1 - holdMax)); + return { + opacity: 1 - t, + transform: `translateY(${-40 * t}vh)`, + }; + } + + if (index === activeStepIndex + 1 && localProgress > holdMax) { + const t = Math.min(1, (localProgress - holdMax) / (1 - holdMax)); + return { + opacity: t, + transform: `translateY(${40 * (1 - t)}vh)`, + }; + } + + return { opacity: 0, transform: 'translateY(40vh)' }; +} + export function LeftColumn({ activeStepIndex, localProgress, @@ -59,22 +130,44 @@ export function LeftColumn({ }: HomeStepperLeftColumnProps) { return ( - - - {steps.map((step, index) => ( - - - - + + + + {steps.map((step, index) => { + const { opacity, transform } = computeDesktopStepStyle( + index, + activeStepIndex, + localProgress, + steps.length, + ); + + return ( + + + + + ); + })} + + + + {steps.map((_, index) => ( + ))} - + ); } diff --git a/packages/twenty-website-new/src/sections/HomeStepper/components/RightColumn/RightColumn.tsx b/packages/twenty-website-new/src/sections/HomeStepper/components/RightColumn/RightColumn.tsx index 3800e8e3960..3c0b7cdee2e 100644 --- a/packages/twenty-website-new/src/sections/HomeStepper/components/RightColumn/RightColumn.tsx +++ b/packages/twenty-website-new/src/sections/HomeStepper/components/RightColumn/RightColumn.tsx @@ -7,9 +7,10 @@ const StyledRightColumn = styled.div` width: 100%; @media (min-width: ${theme.breakpoints.md}px) { + align-self: start; max-width: 672px; position: sticky; - top: ${theme.spacing(10)}; + top: calc(4.5rem + (100vh - 4.5rem) * 0.5 - 368px); } `; diff --git a/packages/twenty-website-new/src/sections/HomeStepper/components/Visual/StepperLottie.tsx b/packages/twenty-website-new/src/sections/HomeStepper/components/Visual/StepperLottie.tsx index b45b654ae15..4191527aecd 100644 --- a/packages/twenty-website-new/src/sections/HomeStepper/components/Visual/StepperLottie.tsx +++ b/packages/twenty-website-new/src/sections/HomeStepper/components/Visual/StepperLottie.tsx @@ -1,9 +1,10 @@ 'use client'; +import { scrollProgressToHomeStepperLottieFrame } from '@/sections/HomeStepper/utils/home-stepper-lottie-frame-map'; import { theme } from '@/theme'; import { styled } from '@linaria/react'; -import Lottie from 'lottie-react'; -import { useEffect, useState } from 'react'; +import Lottie, { type LottieRefCurrentProps } from 'lottie-react'; +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; export const HOME_STEPPER_LOTTIE_SRC = '/images/home/stepper/lottie.json'; @@ -16,17 +17,51 @@ const LottieSlot = styled.div` width: 100%; `; -type LottieAnimationData = Record; +type LottieAnimationData = { + ip?: number; + op?: number; +} & Record; -export function StepperLottie() { - const [animationData, setAnimationData] = useState( - null, +type StepperLottieProps = { + scrollProgress: number; +}; + +function lottieTotalFrameCount(animationData: LottieAnimationData): number { + const op = typeof animationData.op === 'number' ? animationData.op : 0; + const ip = typeof animationData.ip === 'number' ? animationData.ip : 0; + const rounded = Math.round(op - ip); + return rounded >= 1 ? rounded : 0; +} + +function applyScrollToLottie( + lottieApi: LottieRefCurrentProps | null, + scrollProgress: number, + totalFrames: number, +) { + if (!lottieApi?.animationLoaded || totalFrames <= 0) { + return; + } + const frame = scrollProgressToHomeStepperLottieFrame( + scrollProgress, + totalFrames, ); + lottieApi.goToAndStop(frame, true); +} + +export function StepperLottie({ scrollProgress }: StepperLottieProps) { + const [animationData, setAnimationData] = + useState(null); + const [totalFrames, setTotalFrames] = useState(0); + const lottieRef = useRef(null); + const scrollProgressRef = useRef(scrollProgress); + scrollProgressRef.current = scrollProgress; + const totalFramesRef = useRef(totalFrames); + totalFramesRef.current = totalFrames; useEffect(() => { - let cancelled = false; + const controller = new AbortController(); - fetch(HOME_STEPPER_LOTTIE_SRC) + fetch(HOME_STEPPER_LOTTIE_SRC, { signal: controller.signal }) .then((response) => { if (!response.ok) { throw new Error(`Lottie fetch failed: ${response.status}`); @@ -34,21 +69,28 @@ export function StepperLottie() { return response.json() as Promise; }) .then((data) => { - if (!cancelled) { - setAnimationData(data); - } + setAnimationData(data); + setTotalFrames(lottieTotalFrameCount(data)); }) - .catch(() => { - if (!cancelled) { - setAnimationData(null); + .catch((error: unknown) => { + if (error instanceof Error && error.name === 'AbortError') { + return; } + setAnimationData(null); }); return () => { - cancelled = true; + controller.abort(); }; }, []); + useLayoutEffect(() => { + if (!animationData) { + return; + } + applyScrollToLottie(lottieRef.current, scrollProgress, totalFrames); + }, [animationData, scrollProgress, totalFrames]); + if (!animationData) { return ; } @@ -57,8 +99,18 @@ export function StepperLottie() { { + lottieRef.current?.setSubframe(true); + applyScrollToLottie( + lottieRef.current, + scrollProgressRef.current, + totalFramesRef.current, + ); + }} /> ); diff --git a/packages/twenty-website-new/src/sections/HomeStepper/components/Visual/Visual.tsx b/packages/twenty-website-new/src/sections/HomeStepper/components/Visual/Visual.tsx index 55bf318a294..8e9a1aef76d 100644 --- a/packages/twenty-website-new/src/sections/HomeStepper/components/Visual/Visual.tsx +++ b/packages/twenty-website-new/src/sections/HomeStepper/components/Visual/Visual.tsx @@ -6,13 +6,17 @@ import { StepperLottie } from './StepperLottie'; const HOME_STEPPER_BACKGROUND = '/images/home/stepper/background.png'; const HOME_STEPPER_SHAPE = '/images/home/stepper/background-shape.png'; -export function Visual() { +type VisualProps = { + scrollProgress: number; +}; + +export function Visual({ scrollProgress }: VisualProps) { return ( - + ); } diff --git a/packages/twenty-website-new/src/sections/HomeStepper/utils/home-stepper-lottie-frame-map.ts b/packages/twenty-website-new/src/sections/HomeStepper/utils/home-stepper-lottie-frame-map.ts new file mode 100644 index 00000000000..f1ed1dfce1c --- /dev/null +++ b/packages/twenty-website-new/src/sections/HomeStepper/utils/home-stepper-lottie-frame-map.ts @@ -0,0 +1,41 @@ +const STEP_COUNT = 3; + +function clampUnit(value: number): number { + return Math.max(0, Math.min(1, value)); +} + +// Lottie timeline boundaries (from the animation source): +// Step 1 content: frames 0–345, transition 1→2: frames 346–415 +// Step 2 content: frames 416–933, transition 2→3: frames 934–980 +// Step 3 content: frames 981–end +const STEP_1_TRANSITION_END = 415; +const STEP_2_TRANSITION_END = 980; + +// Per-step hold fraction: how much of each step's scroll drives Lottie-only +// (left column stays fixed); the remainder drives the content transition. +export const HOME_STEPPER_HOLD_FRACTIONS = [ + 345 / STEP_1_TRANSITION_END, + (933 - STEP_1_TRANSITION_END) / (STEP_2_TRANSITION_END - STEP_1_TRANSITION_END), + 1, +] as const; + +export function scrollProgressToHomeStepperLottieFrame( + scrollProgress: number, + totalFrames: number, +): number { + const lastIndex = Math.max(0, totalFrames - 1); + + const clamped = clampUnit(scrollProgress); + const stepFloat = clamped * STEP_COUNT; + const stepIndex = Math.min(STEP_COUNT - 1, Math.floor(stepFloat)); + const localProgress = stepFloat - stepIndex; + + const rangeStart = + stepIndex === 0 ? 0 : stepIndex === 1 ? STEP_1_TRANSITION_END : STEP_2_TRANSITION_END; + const rangeEnd = + stepIndex === 0 ? STEP_1_TRANSITION_END : stepIndex === 1 ? STEP_2_TRANSITION_END : lastIndex; + + const frame = rangeStart + localProgress * (rangeEnd - rangeStart); + + return Math.max(0, Math.min(lastIndex, frame)); +} diff --git a/packages/twenty-website-new/src/sections/Menu/components/Root/Root.tsx b/packages/twenty-website-new/src/sections/Menu/components/Root/Root.tsx index 39971ac2cab..c1b1337dd64 100644 --- a/packages/twenty-website-new/src/sections/Menu/components/Root/Root.tsx +++ b/packages/twenty-website-new/src/sections/Menu/components/Root/Root.tsx @@ -16,6 +16,7 @@ import { MenuDrawer } from '../Drawer/Drawer'; const StyledSection = styled.section` backdrop-filter: blur(10px); + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.06); min-width: 0; position: sticky; top: 0; @@ -24,7 +25,8 @@ const StyledSection = styled.section` `; const StyledContainer = styled(Container)` - padding-top: ${theme.spacing(4)}; + padding-top: ${theme.spacing(2)}; + padding-bottom: ${theme.spacing(2)}; position: relative; z-index: 100; `; diff --git a/packages/twenty-website-new/src/sections/PlanTable/components/Content/Content.tsx b/packages/twenty-website-new/src/sections/PlanTable/components/Content/Content.tsx index bc33809ac18..ebd9d9eaaac 100644 --- a/packages/twenty-website-new/src/sections/PlanTable/components/Content/Content.tsx +++ b/packages/twenty-website-new/src/sections/PlanTable/components/Content/Content.tsx @@ -1,4 +1,9 @@ -import { LinkButton } from '@/design-system/components'; +'use client'; + +import { + BaseButton, + buttonBaseStyles, +} from '@/design-system/components/Button/BaseButton'; import { CheckIcon } from '@/icons'; import type { PlanTableCellType, @@ -8,6 +13,7 @@ import type { } from '@/sections/PlanTable/types'; import { theme } from '@/theme'; import { styled } from '@linaria/react'; +import { useState } from 'react'; import { CalculatorEmbed } from '../CalculatorEmbed/CalculatorEmbed'; const TableScope = styled.div` @@ -15,7 +21,6 @@ const TableScope = styled.div` color: ${theme.colors.secondary.text[100]}; display: flex; flex-direction: column; - gap: ${theme.spacing(10)}; width: 100%; `; @@ -94,12 +99,36 @@ const CategoryTitle = styled.span` width: 100%; `; +const CollapsibleWrapper = styled.div` + display: grid; + grid-template-rows: 0fr; + opacity: 0; + transition: + grid-template-rows 0.4s ease, + opacity 0.4s ease; + width: 100%; + + &[data-expanded='true'] { + grid-template-rows: 1fr; + opacity: 1; + } +`; + +const CollapsibleInner = styled.div` + overflow: hidden; +`; + const CtaRow = styled.div` display: flex; justify-content: center; + margin-top: ${theme.spacing(10)}; width: 100%; `; +const ToggleButton = styled.button` + ${buttonBaseStyles} +`; + type CellValueProps = { cell: PlanTableCellType; }; @@ -162,6 +191,44 @@ type ContentProps = { }; export function Content({ data }: ContentProps) { + const [expanded, setExpanded] = useState(false); + + const initialRows = data.rows.slice(0, data.initialVisibleRowCount); + const extraRows = data.rows.slice(data.initialVisibleRowCount); + const hasMoreRows = extraRows.length > 0; + + const toggleLabel = expanded + ? data.seeMoreFeaturesCta.collapseLabel + : data.seeMoreFeaturesCta.expandLabel; + + const mapRows = (rows: PlanTableDataType['rows'], startIndex: number) => + rows.map((row, index) => { + const rowIndex = startIndex + index; + + if (row.type === 'category') { + return ( + + ); + } + + if (row.type === 'calculator') { + return ( + + ); + } + + return ( + + ); + }); + return ( @@ -171,43 +238,27 @@ export function Content({ data }: ContentProps) { ))} - {data.rows.map((row, rowIndex) => { - if (row.type === 'category') { - return ( - + + {mapRows(extraRows, data.initialVisibleRowCount)} + + + )} + + {hasMoreRows && ( + + setExpanded((prev) => !prev)}> + - ); - } - - if (row.type === 'calculator') { - return ( - - ); - } - - return ( - - ); - })} - - - - + + + )} ); } diff --git a/packages/twenty-website-new/src/sections/PlanTable/types/PlanTableData.ts b/packages/twenty-website-new/src/sections/PlanTable/types/PlanTableData.ts index fc264c3f4dd..705801e7cef 100644 --- a/packages/twenty-website-new/src/sections/PlanTable/types/PlanTableData.ts +++ b/packages/twenty-website-new/src/sections/PlanTable/types/PlanTableData.ts @@ -69,10 +69,11 @@ export type PlanTableBodyRowDataType = export type PlanTableDataType = { featureColumnLabel: string; + initialVisibleRowCount: number; rows: PlanTableBodyRowDataType[]; seeMoreFeaturesCta: { - href: string; - label: string; + collapseLabel: string; + expandLabel: string; }; tierColumns: PlanTableTierColumnType[]; }; diff --git a/packages/twenty-website-new/src/sections/ProductStepper/components/Content/Content.tsx b/packages/twenty-website-new/src/sections/ProductStepper/components/Content/Content.tsx index 83f0521ba75..6785ea23dcc 100644 --- a/packages/twenty-website-new/src/sections/ProductStepper/components/Content/Content.tsx +++ b/packages/twenty-website-new/src/sections/ProductStepper/components/Content/Content.tsx @@ -21,6 +21,10 @@ const ContentRoot = styled.div` align-self: stretch; gap: ${theme.spacing(20)}; margin-left: calc(-1 * ${theme.spacing(4)}); + position: sticky; + top: 0; + height: 100vh; + align-items: center; } `; @@ -33,8 +37,6 @@ const StepsColumn = styled.div` @media (min-width: ${theme.breakpoints.md}px) { height: max-content; max-width: 556px; - position: sticky; - top: ${theme.spacing(20)}; } `; diff --git a/packages/twenty-website-new/src/sections/ProductStepper/components/Visual/Visual.tsx b/packages/twenty-website-new/src/sections/ProductStepper/components/Visual/Visual.tsx index f9f5a204fb6..9de5c04f80f 100644 --- a/packages/twenty-website-new/src/sections/ProductStepper/components/Visual/Visual.tsx +++ b/packages/twenty-website-new/src/sections/ProductStepper/components/Visual/Visual.tsx @@ -13,9 +13,10 @@ const VisualColumn = styled.div` width: 100%; @media (min-width: ${theme.breakpoints.md}px) { + align-self: start; max-width: 672px; position: sticky; - top: ${theme.spacing(10)}; + top: calc(4.5rem + (100vh - 4.5rem) * 0.5 - 368px); } `; diff --git a/packages/twenty-website-new/src/sections/Salesforce/components/Flow/Flow.tsx b/packages/twenty-website-new/src/sections/Salesforce/components/Flow/Flow.tsx index 2472ea53c0c..42bbc85205e 100644 --- a/packages/twenty-website-new/src/sections/Salesforce/components/Flow/Flow.tsx +++ b/packages/twenty-website-new/src/sections/Salesforce/components/Flow/Flow.tsx @@ -22,6 +22,10 @@ const CopyColumn = styled.div` gap: ${theme.spacing(2)}; max-width: 400px; min-width: 0; + + @media (min-width: ${theme.breakpoints.md}px) { + align-self: center; + } `; const RightColumn = styled.div` diff --git a/packages/twenty-website-new/src/sections/Salesforce/components/PricingWindow/PricingWindow.tsx b/packages/twenty-website-new/src/sections/Salesforce/components/PricingWindow/PricingWindow.tsx index 40d8e060131..f10016af652 100644 --- a/packages/twenty-website-new/src/sections/Salesforce/components/PricingWindow/PricingWindow.tsx +++ b/packages/twenty-website-new/src/sections/Salesforce/components/PricingWindow/PricingWindow.tsx @@ -7,7 +7,7 @@ import type { } from '@/sections/Salesforce/types'; import { theme } from '@/theme'; import { styled } from '@linaria/react'; -import { useEffect, useRef, useState } from 'react'; +import { useRef } from 'react'; const formatPriceAmount = (amount: number) => `$${new Intl.NumberFormat('en-US').format(amount)}`; @@ -54,10 +54,6 @@ const calculatePriceAmounts = ( }; const PANEL_BACKGROUND = '#c9c9c9'; -const CARD_STICKY_TOP_OFFSET_PX = 64; -const CARD_STICKY_BOTTOM_OFFSET_PX = 340; - -type StickyHeaderMode = 'absolute' | 'fixed'; const Panel = styled.div` background-color: ${PANEL_BACKGROUND}; @@ -69,17 +65,7 @@ const Panel = styled.div` width: 100%; `; -const StickyHeaderSpacer = styled.div<{ $height: number }>` - height: ${({ $height }) => $height}px; - width: 100%; -`; - -const StickyHeader = styled.div<{ - $absoluteTop: number; - $left: number; - $mode: StickyHeaderMode; - $width: number; -}>` +const PricingHeader = styled.div` background-color: ${PANEL_BACKGROUND}; box-shadow: inset 1px 0 0 0 #dfdfdf, @@ -88,11 +74,8 @@ const StickyHeader = styled.div<{ inset -2px 0 0 0 #808080, inset 0 1px 0 0 #dfdfdf, inset 0 2px 0 0 #ffffff; - left: ${({ $left, $mode }) => ($mode === 'fixed' ? `${$left}px` : '0')}; - position: ${({ $mode }) => ($mode === 'fixed' ? 'fixed' : 'absolute')}; - top: ${({ $absoluteTop, $mode }) => - $mode === 'fixed' ? `${CARD_STICKY_TOP_OFFSET_PX}px` : `${$absoluteTop}px`}; - width: ${({ $mode, $width }) => ($mode === 'fixed' ? `${$width}px` : '100%')}; + position: relative; + width: 100%; z-index: 20; `; @@ -437,139 +420,20 @@ export type PricingWindowProps = { pricing: SalesforcePricingPanelType; }; -type StickyHeaderState = { - absoluteTop: number; - height: number; - left: number; - mode: StickyHeaderMode; - width: number; -}; - export function PricingWindow({ checkedIds, onAddonToggle, onClose, pricing, }: PricingWindowProps) { - const panelRef = useRef(null); - const stickyHeaderRef = useRef(null); const addonAnchorRefs = useRef>({}); - const [stickyHeaderState, setStickyHeaderState] = useState( - { - absoluteTop: 0, - height: 0, - left: 0, - mode: 'absolute', - width: 0, - }, - ); const { fixedPriceAmount, perSeatPriceAmount, totalPriceAmount } = calculatePriceAmounts(pricing, checkedIds); - useEffect(() => { - let frameId = 0; - - const updateStickyHeaderState = () => { - frameId = 0; - - const panel = panelRef.current; - const stickyHeader = stickyHeaderRef.current; - - if (!panel || !stickyHeader) { - return; - } - - const panelRect = panel.getBoundingClientRect(); - const stickyHeight = stickyHeader.offsetHeight; - const panelHeight = panel.offsetHeight; - const panelTop = window.scrollY + panelRect.top; - const maxAbsoluteTop = Math.max( - 0, - panelHeight - stickyHeight - CARD_STICKY_BOTTOM_OFFSET_PX, - ); - const fixedEndScrollY = - panelTop + - panelHeight - - stickyHeight - - CARD_STICKY_TOP_OFFSET_PX - - CARD_STICKY_BOTTOM_OFFSET_PX; - const nextState: StickyHeaderState = - window.scrollY >= panelTop - CARD_STICKY_TOP_OFFSET_PX && - window.scrollY < fixedEndScrollY - ? { - absoluteTop: 0, - height: stickyHeight, - left: panelRect.left, - mode: 'fixed', - width: panelRect.width, - } - : { - absoluteTop: - window.scrollY >= fixedEndScrollY ? maxAbsoluteTop : 0, - height: stickyHeight, - left: 0, - mode: 'absolute', - width: panelRect.width, - }; - - setStickyHeaderState((previous) => - previous.absoluteTop === nextState.absoluteTop && - previous.height === nextState.height && - previous.left === nextState.left && - previous.mode === nextState.mode && - previous.width === nextState.width - ? previous - : nextState, - ); - }; - - const requestStickyHeaderUpdate = () => { - if (frameId !== 0) { - return; - } - - frameId = window.requestAnimationFrame(updateStickyHeaderState); - }; - - requestStickyHeaderUpdate(); - - window.addEventListener('resize', requestStickyHeaderUpdate); - window.addEventListener('scroll', requestStickyHeaderUpdate, { - passive: true, - }); - - const resizeObserver = new ResizeObserver(requestStickyHeaderUpdate); - - if (panelRef.current) { - resizeObserver.observe(panelRef.current); - } - - if (stickyHeaderRef.current) { - resizeObserver.observe(stickyHeaderRef.current); - } - - return () => { - if (frameId !== 0) { - window.cancelAnimationFrame(frameId); - } - - resizeObserver.disconnect(); - window.removeEventListener('resize', requestStickyHeaderUpdate); - window.removeEventListener('scroll', requestStickyHeaderUpdate); - }; - }, [fixedPriceAmount]); - return ( - +