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 (
-
+
-
-
+
{pricing.windowTitle}
@@ -621,7 +485,7 @@ export function PricingWindow({
-
+
{pricing.featureSectionHeading}
diff --git a/packages/twenty-website-new/src/sections/ThreeCards/components/FeatureCards/FeatureCards.tsx b/packages/twenty-website-new/src/sections/ThreeCards/components/FeatureCards/FeatureCards.tsx
index e72a9e43305..aa466d64ed4 100644
--- a/packages/twenty-website-new/src/sections/ThreeCards/components/FeatureCards/FeatureCards.tsx
+++ b/packages/twenty-website-new/src/sections/ThreeCards/components/FeatureCards/FeatureCards.tsx
@@ -1,6 +1,10 @@
+'use client';
+
+import { ThreeCardsScrollLayoutEffect } from '@/sections/ThreeCards/effect-components/ThreeCardsScrollLayoutEffect';
import { ThreeCardsFeatureCardType } from '@/sections/ThreeCards/types';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
+import { useRef } from 'react';
import { FeatureCard } from '../FeatureCard/FeatureCard';
const FeatureCardsGrid = styled.div`
@@ -15,13 +19,32 @@ const FeatureCardsGrid = styled.div`
}
`;
+const CardSlot = styled.div`
+ will-change: transform, opacity;
+`;
+
type FeatureCardsProps = { featureCards: ThreeCardsFeatureCardType[] };
export function FeatureCards({ featureCards }: FeatureCardsProps) {
+ const gridRef = useRef(null);
+ const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
+
return (
-
+
+
{featureCards.map((featureCard, index) => (
-
+ {
+ cardRefs.current[index] = element;
+ }}
+ >
+
+
))}
);
diff --git a/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCard/IllustrationCard.tsx b/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCard/IllustrationCard.tsx
index 1296e36187f..cea176994a6 100644
--- a/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCard/IllustrationCard.tsx
+++ b/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCard/IllustrationCard.tsx
@@ -1,7 +1,6 @@
'use client';
-import { Body, Heading, IconButton } from '@/design-system/components';
-import { ArrowRightIcon } from '@/icons';
+import { Body, Heading } from '@/design-system/components';
import { THREE_CARDS_ILLUSTRATIONS } from '@/illustrations';
import type { ThreeCardsIllustrationCardType } from '@/sections/ThreeCards/types';
import { theme } from '@/theme';
@@ -40,10 +39,9 @@ const CardEmbed = styled.div`
`;
const CardFooter = styled.footer`
- display: grid;
- grid-template-columns: auto auto auto 1fr;
- align-items: start;
- column-gap: ${theme.spacing(2)};
+ display: flex;
+ align-items: center;
+ gap: ${theme.spacing(2)};
`;
const AttributionPipe = styled.span`
@@ -53,10 +51,6 @@ const AttributionPipe = styled.span`
border-left: 1px solid ${theme.colors.primary.border[20]};
`;
-const FooterTrailingAction = styled.div`
- justify-self: end;
-`;
-
const CardBodyCell = styled.div`
align-self: start;
min-height: 0;
@@ -111,17 +105,6 @@ export function IllustrationCard({
size="xs"
weight="regular"
/>
-
-
-
)}
diff --git a/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCards/IllustrationCards.tsx b/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCards/IllustrationCards.tsx
index 571b1bde3c7..6e6cc9af24f 100644
--- a/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCards/IllustrationCards.tsx
+++ b/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCards/IllustrationCards.tsx
@@ -1,6 +1,10 @@
+'use client';
+
+import { ThreeCardsScrollLayoutEffect } from '@/sections/ThreeCards/effect-components/ThreeCardsScrollLayoutEffect';
import { ThreeCardsIllustrationCardType } from '@/sections/ThreeCards/types';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
+import { useRef } from 'react';
import { IllustrationCard } from '../IllustrationCard/IllustrationCard';
const IllustrationCardsGrid = styled.div`
@@ -15,6 +19,10 @@ const IllustrationCardsGrid = styled.div`
}
`;
+const CardSlot = styled.div`
+ will-change: transform, opacity;
+`;
+
type IllustrationCardsProps = {
illustrationCards: ThreeCardsIllustrationCardType[];
variant?: 'shaped' | 'simple';
@@ -24,14 +32,28 @@ export function IllustrationCards({
illustrationCards,
variant = 'shaped',
}: IllustrationCardsProps) {
+ const gridRef = useRef(null);
+ const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
+
return (
-
+
+
{illustrationCards.map((illustrationCard, index) => (
-
+ ref={(element) => {
+ cardRefs.current[index] = element;
+ }}
+ >
+
+
))}
);
diff --git a/packages/twenty-website-new/src/sections/ThreeCards/effect-components/ThreeCardsScrollLayoutEffect.tsx b/packages/twenty-website-new/src/sections/ThreeCards/effect-components/ThreeCardsScrollLayoutEffect.tsx
new file mode 100644
index 00000000000..99eb34ebdd8
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/ThreeCards/effect-components/ThreeCardsScrollLayoutEffect.tsx
@@ -0,0 +1,49 @@
+'use client';
+
+import { useEffect } from 'react';
+
+import {
+ applyThreeCardsScrollLayout,
+ type ThreeCardsScrollLayoutRefs,
+} from '@/sections/ThreeCards/utils/three-cards-scroll-layout';
+
+type ThreeCardsScrollLayoutEffectProps = ThreeCardsScrollLayoutRefs & {
+ cardCount: number;
+};
+
+export function ThreeCardsScrollLayoutEffect({
+ cardCount,
+ cardRefs,
+ gridRef,
+}: ThreeCardsScrollLayoutEffectProps) {
+ useEffect(() => {
+ const refs: ThreeCardsScrollLayoutRefs = { cardRefs, gridRef };
+
+ let rafId: number | null = null;
+
+ const flushLayout = () => {
+ rafId = null;
+ applyThreeCardsScrollLayout(refs, cardCount);
+ };
+
+ const scheduleLayout = () => {
+ if (rafId !== null) {
+ return;
+ }
+ rafId = window.requestAnimationFrame(flushLayout);
+ };
+
+ applyThreeCardsScrollLayout(refs, cardCount);
+ window.addEventListener('scroll', scheduleLayout, { passive: true });
+ window.addEventListener('resize', scheduleLayout);
+ return () => {
+ window.removeEventListener('scroll', scheduleLayout);
+ window.removeEventListener('resize', scheduleLayout);
+ if (rafId !== null) {
+ window.cancelAnimationFrame(rafId);
+ }
+ };
+ }, [cardCount, cardRefs, gridRef]);
+
+ return null;
+}
diff --git a/packages/twenty-website-new/src/sections/ThreeCards/utils/three-cards-scroll-layout.ts b/packages/twenty-website-new/src/sections/ThreeCards/utils/three-cards-scroll-layout.ts
new file mode 100644
index 00000000000..52e51540be4
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/ThreeCards/utils/three-cards-scroll-layout.ts
@@ -0,0 +1,69 @@
+import type { RefObject } from 'react';
+
+import { theme } from '@/theme';
+
+function clamp01(value: number) {
+ return Math.min(1, Math.max(0, value));
+}
+
+function easeOutQuint(t: number) {
+ return 1 - Math.pow(1 - t, 5);
+}
+
+export type ThreeCardsScrollLayoutRefs = {
+ cardRefs: RefObject<(HTMLDivElement | null)[]>;
+ gridRef: RefObject;
+};
+
+const STAGGER = 0.25;
+
+export function applyThreeCardsScrollLayout(
+ refs: ThreeCardsScrollLayoutRefs,
+ cardCount: number,
+): void {
+ const grid = refs.gridRef.current;
+ if (!grid) {
+ return;
+ }
+
+ const isDesktop = window.matchMedia(
+ `(min-width: ${theme.breakpoints.md}px)`,
+ ).matches;
+
+ if (!isDesktop) {
+ for (let index = 0; index < cardCount; index++) {
+ const node = refs.cardRefs.current[index];
+ if (!node) {
+ continue;
+ }
+ node.style.opacity = '1';
+ node.style.transform = 'none';
+ }
+ return;
+ }
+
+ const rect = grid.getBoundingClientRect();
+ const viewportHeight = window.innerHeight;
+
+ const startEdge = viewportHeight;
+ const endEdge = viewportHeight * 0.2;
+ const progress = clamp01((startEdge - rect.top) / (startEdge - endEdge));
+
+ for (let index = 0; index < cardCount; index++) {
+ const node = refs.cardRefs.current[index];
+ if (!node) {
+ continue;
+ }
+
+ const delay = index * STAGGER;
+ const localProgress = clamp01((progress - delay) / (1 - delay));
+ const eased = easeOutQuint(localProgress);
+
+ const translateY = (1 - eased) * 200;
+ const scale = 0.88 + eased * 0.12;
+ const opacity = clamp01(localProgress / 0.4);
+
+ node.style.opacity = String(opacity);
+ node.style.transform = `translateY(${translateY}px) scale(${scale})`;
+ }
+}
diff --git a/packages/twenty-website-new/src/sections/WhyTwentyStepper/components/Content/Content.tsx b/packages/twenty-website-new/src/sections/WhyTwentyStepper/components/Content/Content.tsx
index 1e3a443342a..799cee1b851 100644
--- a/packages/twenty-website-new/src/sections/WhyTwentyStepper/components/Content/Content.tsx
+++ b/packages/twenty-website-new/src/sections/WhyTwentyStepper/components/Content/Content.tsx
@@ -17,6 +17,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;
}
`;
@@ -29,8 +33,6 @@ const StepsColumn = styled.div`
@media (min-width: ${theme.breakpoints.md}px) {
height: max-content;
max-width: 556px;
- position: sticky;
- top: calc(50vh - 150px);
}
`;
diff --git a/packages/twenty-website-new/src/sections/WhyTwentyStepper/components/Flow/Flow.tsx b/packages/twenty-website-new/src/sections/WhyTwentyStepper/components/Flow/Flow.tsx
index 278a176c114..9665f74379f 100644
--- a/packages/twenty-website-new/src/sections/WhyTwentyStepper/components/Flow/Flow.tsx
+++ b/packages/twenty-website-new/src/sections/WhyTwentyStepper/components/Flow/Flow.tsx
@@ -3,10 +3,24 @@
import { IllustrationMount } from '@/illustrations';
import { SyncScrollProgressFromContainerEffect } from '@/sections/WhyTwentyStepper/effect-components/SyncScrollProgressFromContainerEffect';
import type { WhyTwentyStepperDataType } from '@/sections/WhyTwentyStepper/types';
+import { theme } from '@/theme';
+import { styled } from '@linaria/react';
import { useRef, useState } from 'react';
import { Content } from '../Content/Content';
import { Root } from '../Root/Root';
+const IllustrationColumn = styled.div`
+ min-width: 0;
+ width: 100%;
+
+ @media (min-width: ${theme.breakpoints.md}px) {
+ align-self: start;
+ max-width: 672px;
+ position: sticky;
+ top: calc(4.5rem + (100vh - 4.5rem) * 0.5 - 368px);
+ }
+`;
+
type FlowProps = WhyTwentyStepperDataType;
export function Flow({ body, heading, illustration }: FlowProps) {
@@ -32,7 +46,9 @@ export function Flow({ body, heading, illustration }: FlowProps) {
heading={heading}
localProgress={localProgress}
/>
-
+
+
+
);
}