diff --git a/packages/twenty-website-new/public/images/pricing/salesforce/help-icon.png b/packages/twenty-website-new/public/images/pricing/salesforce/help-icon.png
new file mode 100644
index 0000000000..4befdf9b08
Binary files /dev/null and b/packages/twenty-website-new/public/images/pricing/salesforce/help-icon.png differ
diff --git a/packages/twenty-website-new/src/app/layout.tsx b/packages/twenty-website-new/src/app/layout.tsx
index 8c9e20aa98..88de054bbd 100644
--- a/packages/twenty-website-new/src/app/layout.tsx
+++ b/packages/twenty-website-new/src/app/layout.tsx
@@ -8,7 +8,7 @@ import { cssVariables } from '@/theme/css-variables';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import type { Metadata } from 'next';
-import { Aleo, Azeret_Mono, Host_Grotesk } from 'next/font/google';
+import { Aleo, Azeret_Mono, Host_Grotesk, VT323 } from 'next/font/google';
import '../../../twenty-ui/dist/theme-light.css';
const hostGrotesk = Host_Grotesk({
@@ -32,6 +32,13 @@ const azeretMono = Azeret_Mono({
display: 'swap',
});
+const vt323 = VT323({
+ subsets: ['latin'],
+ weight: '400',
+ variable: '--font-retro',
+ display: 'swap',
+});
+
css`
:global(*),
:global(*::before),
@@ -77,7 +84,7 @@ export default async function RootLayout({
return (
{children}
diff --git a/packages/twenty-website-new/src/app/pricing/_constants/salesforce.ts b/packages/twenty-website-new/src/app/pricing/_constants/salesforce.ts
index c59585d475..0711b4549c 100644
--- a/packages/twenty-website-new/src/app/pricing/_constants/salesforce.ts
+++ b/packages/twenty-website-new/src/app/pricing/_constants/salesforce.ts
@@ -1,5 +1,7 @@
import type { SalesforceDataType } from '@/sections/Salesforce/types';
+const SALESFORCE_POPUP_TITLE = 'Good choice!';
+
export const SALESFORCE_DATA: SalesforceDataType = {
body: {
text: 'Aenean lacinia bibendum nulla sed consectetur. Integer posuere erat a ante venenatis dapibus.',
@@ -11,56 +13,198 @@ export const SALESFORCE_DATA: SalesforceDataType = {
pricing: {
addons: [
{
+ cost: 35,
id: 'api-access',
label: 'API access',
- popupKey: 'wrongChoiceDefault',
+ popup: {
+ body: 'APIs are extra. Simplicity has a price.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
rightLabel: '+$35/user per month',
},
{
+ cost: 0,
+ fixedCost: 7000,
id: 'webhooks',
- label: 'Webhooks (Change data capture)',
- popupKey: 'wrongChoiceLorem',
+ label: 'Webhooks (Change Data Capture)',
+ popup: {
+ body: 'Real-time changes? That will be a premium surprise.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
rightLabel: '+$7000/org per month',
},
{
+ cost: 0,
disabled: true,
id: 'live-updates',
label: 'Live updates',
- popupKey: 'wrongChoiceDefault',
+ popup: {
+ body: 'Live updates are unavailable, which is almost more honest.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
rightLabel: 'Unavailable',
},
{
+ cost: 0,
defaultChecked: true,
+ disabled: true,
id: 'ui-theme',
label: 'UI theme',
- popupKey: 'wrongChoiceDefault',
+ popup: {
+ body: 'A retro theme as a paid add-on is somehow the most believable part.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
rightLabel: 'Retro 2015',
},
{
+ cost: 5,
id: 'sso',
label: 'SSO',
- popupKey: 'wrongChoiceDefault',
+ popup: {
+ body: 'Secure logins cost extra. Naturally.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
rightLabel: '+$5/user per month',
},
{
+ cost: 75,
id: 'permissions',
- label: '11 permissions groups',
- popupKey: 'wrongChoiceDefault',
- rightLabel: '+$75/user per month (Switch to enterprise!)',
+ label: '11 permissions\ngroups',
+ popup: {
+ body: 'Granular permissions live behind yet another paywall.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
+ rightLabel: '+$75/user per month\nSwitch to enterprise!',
+ sharedCostKey: 'enterprise-plan',
},
{
+ cost: 105,
id: 'maps',
label: 'Maps view',
- popupKey: 'wrongChoiceDefault',
+ popup: {
+ body: 'Apparently even seeing your deals on a map is a luxury.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
rightLabel: '+$105/user per month',
},
+ {
+ cost: 75,
+ id: 'workflows',
+ label: '6 workflows',
+ popup: {
+ body: 'Workflow automation stops being basic the second it becomes useful.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
+ rightLabel: '+$75/user per month\nSwitch to enterprise!',
+ sharedCostKey: 'enterprise-plan',
+ },
+ {
+ cost: 0,
+ id: 'lock-in',
+ label: 'Lock-in',
+ popup: {
+ body: 'The discount gets better the harder it is to leave.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
+ rightLabel: '3 2 years contract\n-33% off',
+ rightLabelParts: [
+ [{ strike: true, text: '3' }, { text: ' 2 years contract' }],
+ [{ text: '-33% off' }],
+ ],
+ },
+ {
+ cost: 0,
+ defaultChecked: true,
+ disabled: true,
+ id: 'apex-tutorials',
+ label: 'APEX tutorials',
+ popup: {
+ body: 'Even the training material is a feature worth celebrating.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
+ rightLabel: 'Free for you!',
+ },
+ {
+ cost: 0,
+ disabled: true,
+ id: 'self-hosting',
+ label: 'Self-hosting',
+ popup: {
+ body: 'Owning your stack remains mysteriously out of stock.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
+ rightLabel: 'Out of stock',
+ },
+ {
+ cost: 0,
+ defaultChecked: true,
+ disabled: true,
+ id: 'salesforce-classic',
+ label: 'Salesforce Classic',
+ popup: {
+ body: 'Classic never dies. It just gets extended one more time.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
+ rightLabel: 'Extended run!',
+ },
+ {
+ cost: 75,
+ id: 'flow-orchestration',
+ label: 'Flow\norchestration',
+ popup: {
+ body: 'Orchestration brings its own fee schedule, naturally.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
+ rightLabel:
+ '$1/orchestration run/org\n+$75/user per month\nSwitch to enterprise!',
+ sharedCostKey: 'enterprise-plan',
+ },
+ {
+ cost: 0,
+ disabled: true,
+ id: 'infinite-scroll',
+ label: 'Infinite scroll',
+ popup: {
+ body: 'Infinite scroll is still coming soon, unlike the invoice.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
+ rightLabel: 'Coming soon!',
+ },
+ {
+ cost: 75,
+ id: 'ai-einstein',
+ label: 'AI (Einstein)',
+ popup: {
+ body: 'AI is available right after an enterprise-sized upgrade.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
+ rightLabel: '+$75/user per month',
+ },
+ {
+ cost: 75,
+ id: 'encrypt-data',
+ label: 'Encrypt your data',
+ netSpendRate: 0.2,
+ popup: {
+ body: 'Data protection is packaged like an optional luxury.',
+ titleBar: SALESFORCE_POPUP_TITLE,
+ },
+ rightLabel:
+ '+20% of net spend\n+$75/user per month\nSwitch to enterprise!',
+ sharedCostKey: 'enterprise-plan',
+ },
],
+ basePriceAmount: 100,
featureSectionHeading: 'Best for Salesforce',
- priceAmount: '$100 USD',
+ productIconAlt: 'Retro help document icon',
+ productIconSrc: '/images/pricing/salesforce/help-icon.png',
priceSuffix: ' / seat / month - billed yearly',
- primaryCtaLabel: 'Do you really want to click and ask your first quote ???',
productTitle: 'Salesforce Pro',
- secondaryCtaLabel: "More options available, BUT DON'T CLICK !!",
+ secondaryCtaNote: 'More options available!',
+ secondaryCtaHref:
+ 'https://www.salesforce.com/en-us/wp-content/uploads/sites/4/documents/pricing/all-add-ons.pdf',
+ secondaryCtaLabel: 'Check more add-ons',
+ totalPriceLabel: 'total per month with fixed cost',
windowTitle: 'Salesforce Pro pricing',
},
};
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 b00883a60d..39971ac2ca 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
@@ -15,8 +15,12 @@ import { CloseDrawerWhenNavigationExpandsEffect } from '../../effect-components/
import { MenuDrawer } from '../Drawer/Drawer';
const StyledSection = styled.section`
+ backdrop-filter: blur(10px);
min-width: 0;
+ position: sticky;
+ top: 0;
width: 100%;
+ z-index: 200;
`;
const StyledContainer = styled(Container)`
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 bdede0352a..2472ea53c0 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
@@ -1,17 +1,20 @@
'use client';
import { Body, Heading } from '@/design-system/components';
-import { SALESFORCE_POPUPS } from '@/sections/Salesforce/constants';
import type {
SalesforceAddonRowType,
SalesforceDataType,
+ SalesforceWrongChoicePopupType,
} from '@/sections/Salesforce/types';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
-import { useCallback, useState } from 'react';
+import { useCallback, useRef, useState } from 'react';
import { PricingWindow } from '../PricingWindow/PricingWindow';
import { Root } from '../Root/Root';
-import { WrongChoicePopup } from '../WrongChoicePopup/WrongChoicePopup';
+import {
+ WrongChoicePopup,
+ WRONG_CHOICE_POPUP_WIDTH,
+} from '../WrongChoicePopup/WrongChoicePopup';
const CopyColumn = styled.div`
display: flex;
@@ -29,23 +32,66 @@ const RightColumn = styled.div`
`;
type OpenWrongChoicePopup = {
- addonId: string;
body: string;
key: string;
- stackIndex: number;
+ layerIndex: number;
+ left: number;
+ sourceId: string;
+ top: number;
titleBar: string;
};
-const reindexPopups = (
- list: OpenWrongChoicePopup[],
-): OpenWrongChoicePopup[] =>
- list.map((popup, index) => ({ ...popup, stackIndex: index }));
+const POPUP_MARGIN = 12;
+const POPUP_X_OFFSET = 32;
+const POPUP_Y_OFFSET = 12;
+const POPUP_STACK_OFFSET = 14;
+
+const getPopupPosition = (
+ anchorRect: DOMRect | null,
+ containerRect: DOMRect | null,
+ stackIndex = 0,
+) => {
+ if (!anchorRect || !containerRect) {
+ return {
+ left: 24 + stackIndex * POPUP_STACK_OFFSET,
+ top: 120 + stackIndex * POPUP_STACK_OFFSET,
+ };
+ }
+
+ const maxLeft = Math.max(
+ POPUP_MARGIN,
+ containerRect.width - WRONG_CHOICE_POPUP_WIDTH - POPUP_MARGIN,
+ );
+
+ return {
+ left: Math.min(
+ Math.max(
+ anchorRect.left -
+ containerRect.left +
+ POPUP_X_OFFSET +
+ stackIndex * POPUP_STACK_OFFSET,
+ POPUP_MARGIN,
+ ),
+ maxLeft,
+ ),
+ top: Math.max(
+ anchorRect.top -
+ containerRect.top -
+ POPUP_Y_OFFSET +
+ stackIndex * POPUP_STACK_OFFSET,
+ POPUP_MARGIN,
+ ),
+ };
+};
type FlowProps = SalesforceDataType & {
backgroundColor: string;
};
export function Flow({ backgroundColor, body, heading, pricing }: FlowProps) {
+ const rightColumnRef = useRef(null);
+ const popupSequenceRef = useRef(0);
+ const [isPricingWindowVisible, setIsPricingWindowVisible] = useState(true);
const [checkedIds, setCheckedIds] = useState(() => {
const initial = new Set();
for (const row of pricing.addons) {
@@ -58,22 +104,51 @@ export function Flow({ backgroundColor, body, heading, pricing }: FlowProps) {
const [popups, setPopups] = useState([]);
+ const openPopup = useCallback(
+ (
+ sourceId: string,
+ popup: SalesforceWrongChoicePopupType,
+ anchorRect: DOMRect | null,
+ ) => {
+ const popupSequence = popupSequenceRef.current;
+ popupSequenceRef.current += 1;
+
+ setPopups((previous) => {
+ const popupCountForSource = previous.filter(
+ (entry) => entry.sourceId === sourceId,
+ ).length;
+ const popupPosition = getPopupPosition(
+ anchorRect,
+ rightColumnRef.current?.getBoundingClientRect() ?? null,
+ popupCountForSource,
+ );
+
+ return [
+ ...previous,
+ {
+ body: popup.body,
+ key: `${sourceId}-${popupSequence}`,
+ layerIndex: popupSequence,
+ left: popupPosition.left,
+ sourceId,
+ top: popupPosition.top,
+ titleBar: popup.titleBar,
+ },
+ ];
+ });
+ },
+ [],
+ );
+
const handleAddonToggle = useCallback(
- (addon: SalesforceAddonRowType) => {
+ (addon: SalesforceAddonRowType, anchorRect: DOMRect | null) => {
if (addon.disabled) {
return;
}
- const popupTemplate = SALESFORCE_POPUPS[addon.popupKey];
- if (!popupTemplate) {
- return;
- }
-
- const wasChecked = checkedIds.has(addon.id);
-
setCheckedIds((previous) => {
const next = new Set(previous);
- if (wasChecked) {
+ if (next.has(addon.id)) {
next.delete(addon.id);
} else {
next.add(addon.id);
@@ -81,32 +156,18 @@ export function Flow({ backgroundColor, body, heading, pricing }: FlowProps) {
return next;
});
- if (wasChecked) {
- setPopups((previous) =>
- reindexPopups(
- previous.filter((popup) => popup.addonId !== addon.id),
- ),
- );
- } else {
- setPopups((previous) => [
- ...previous,
- {
- addonId: addon.id,
- body: popupTemplate.body,
- key: `${addon.id}-${Date.now()}`,
- stackIndex: previous.length,
- titleBar: popupTemplate.titleBar,
- },
- ]);
- }
+ openPopup(addon.id, addon.popup, anchorRect);
},
- [checkedIds],
+ [openPopup],
);
const handleClosePopup = useCallback((key: string) => {
- setPopups((previous) =>
- reindexPopups(previous.filter((popup) => popup.key !== key)),
- );
+ setPopups((previous) => previous.filter((popup) => popup.key !== key));
+ }, []);
+
+ const handleClosePricingWindow = useCallback(() => {
+ setIsPricingWindowVisible(false);
+ setPopups([]);
}, []);
return (
@@ -115,24 +176,31 @@ export function Flow({ backgroundColor, body, heading, pricing }: FlowProps) {
-
-
- {popups.map((popup) => (
- {
- handleClosePopup(popup.key);
- }}
- stackIndex={popup.stackIndex}
- titleBar={popup.titleBar}
- titleId={`sf-wrong-choice-${popup.key}`}
+
+ {isPricingWindowVisible ? (
+
- ))}
+ ) : null}
+ {isPricingWindowVisible
+ ? popups.map((popup) => (
+ {
+ handleClosePopup(popup.key);
+ }}
+ top={popup.top}
+ titleBar={popup.titleBar}
+ titleId={`sf-wrong-choice-${popup.key}`}
+ />
+ ))
+ : null}
);
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 f0c7e43c26..0d5a022383 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
@@ -3,12 +3,64 @@
import type {
SalesforceAddonRowType,
SalesforcePricingPanelType,
+ SalesforceRichTextPartType,
} from '@/sections/Salesforce/types';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
+import { useEffect, useRef, useState } from 'react';
+
+const formatPriceAmount = (amount: number) =>
+ `$${new Intl.NumberFormat('en-US').format(amount)}`;
+
+const calculatePriceAmounts = (
+ pricing: SalesforcePricingPanelType,
+ checkedIds: ReadonlySet,
+) => {
+ const appliedSharedCosts = new Set();
+
+ let perSeatBaseAmount = pricing.basePriceAmount;
+ let fixedPriceAmount = 0;
+ let netSpendRate = 0;
+
+ for (const addon of pricing.addons) {
+ if (!checkedIds.has(addon.id)) {
+ continue;
+ }
+
+ if (addon.sharedCostKey && appliedSharedCosts.has(addon.sharedCostKey)) {
+ fixedPriceAmount += addon.fixedCost ?? 0;
+ netSpendRate += addon.netSpendRate ?? 0;
+ continue;
+ }
+
+ if (addon.sharedCostKey) {
+ appliedSharedCosts.add(addon.sharedCostKey);
+ }
+
+ perSeatBaseAmount += addon.cost;
+ fixedPriceAmount += addon.fixedCost ?? 0;
+ netSpendRate += addon.netSpendRate ?? 0;
+ }
+
+ const perSeatPriceAmount = perSeatBaseAmount * (1 + netSpendRate);
+ const totalBaseAmount = perSeatBaseAmount + fixedPriceAmount;
+ const totalPriceAmount = totalBaseAmount * (1 + netSpendRate);
+
+ return {
+ fixedPriceAmount,
+ perSeatPriceAmount,
+ totalPriceAmount,
+ };
+};
+
+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: rgba(28, 28, 28, 0.2);
+ background-color: ${PANEL_BACKGROUND};
display: flex;
flex-direction: column;
max-width: 672px;
@@ -17,6 +69,33 @@ 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;
+}>`
+ background-color: ${PANEL_BACKGROUND};
+ box-shadow:
+ inset 1px 0 0 0 #dfdfdf,
+ inset 2px 0 0 0 #ffffff,
+ inset -1px 0 0 0 #0a0a0a,
+ 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%')};
+ z-index: 20;
+`;
+
const TitleBar = styled.div`
align-items: center;
background: linear-gradient(90deg, #000080 0%, #1084d0 100%);
@@ -27,13 +106,45 @@ const TitleBar = styled.div`
`;
const TitleBarText = styled.p`
- color: ${theme.colors.secondary.background[100]};
- font-family: ${theme.font.family.mono};
+ color: ${theme.colors.secondary.text[100]};
+ font-family: ${theme.font.family.retro};
font-size: ${theme.font.size(4)};
line-height: 12px;
margin: 0;
`;
+const TitleBarActions = styled.div`
+ display: flex;
+ gap: 2px;
+`;
+
+const TitleBarActionButton = styled.button`
+ align-items: center;
+ background: rgba(255, 255, 255, 0.2);
+ border: none;
+ box-shadow:
+ inset -1px -1px 0 0 #0a0a0a,
+ inset 1px 1px 0 0 #ffffff,
+ inset -2px -2px 0 0 #808080,
+ inset 2px 2px 0 0 #dfdfdf;
+ color: ${theme.colors.primary.text[100]};
+ cursor: pointer;
+ display: flex;
+ font-family: ${theme.font.family.retro};
+ font-size: ${theme.font.size(3.5)};
+ justify-content: center;
+ line-height: 1;
+ min-height: 18px;
+ min-width: 18px;
+ padding: 2px 4px;
+
+ &:active {
+ box-shadow:
+ inset 1px 1px 0 0 #0a0a0a,
+ inset -1px -1px 0 0 #ffffff;
+ }
+`;
+
const WindowChrome = styled.div`
box-shadow:
inset -1px -1px 0 0 #0a0a0a,
@@ -46,7 +157,7 @@ const WindowChrome = styled.div`
`;
const ContentPad = styled.div`
- padding: ${theme.spacing(2.75)};
+ padding: 0 ${theme.spacing(2.75)} ${theme.spacing(2.75)};
width: 100%;
`;
@@ -59,12 +170,42 @@ const Inner = styled.div`
width: 100%;
`;
+const SummaryPad = styled.div`
+ padding: ${theme.spacing(2.75)} ${theme.spacing(2.75)} 0;
+ width: 100%;
+`;
+
+const SummaryInner = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${theme.spacing(4)};
+ padding: ${theme.spacing(4)} ${theme.spacing(4)} 0;
+ width: 100%;
+`;
+
const ProductBlock = styled.div`
display: flex;
flex-direction: column;
- font-family: ${theme.font.family.mono};
+ font-family: ${theme.font.family.retro};
+ gap: ${theme.spacing(4)};
+ width: 100%;
+`;
+
+const ProductHeader = styled.div`
+ align-items: flex-start;
+ display: flex;
+ gap: ${theme.spacing(4)};
+ justify-content: space-between;
+ width: 100%;
+`;
+
+const ProductCopy = styled.div`
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
gap: ${theme.spacing(4)};
max-width: 427px;
+ min-width: 0;
`;
const ProductTitle = styled.p`
@@ -94,7 +235,35 @@ const PriceSuffix = styled.span`
line-height: ${theme.spacing(5.5)};
`;
-const FakeButton = styled.div`
+const TotalPriceRow = styled.div`
+ align-items: baseline;
+ display: flex;
+ gap: ${theme.spacing(2)};
+ white-space: nowrap;
+ width: 100%;
+`;
+
+const TotalPriceAmount = styled.span`
+ color: ${theme.colors.primary.text[100]};
+ font-size: ${theme.font.size(8)};
+ line-height: ${theme.spacing(10)};
+`;
+
+const TotalPriceLabel = styled.span`
+ color: ${theme.colors.primary.text[60]};
+ font-size: ${theme.font.size(4.5)};
+ line-height: ${theme.spacing(5.5)};
+`;
+
+const ProductIcon = styled.img`
+ flex-shrink: 0;
+ height: 92px;
+ image-rendering: pixelated;
+ object-fit: contain;
+ width: 92px;
+`;
+
+const FakeButton = styled.a`
align-items: center;
background-color: rgba(28, 28, 28, 0.2);
box-shadow:
@@ -104,19 +273,32 @@ const FakeButton = styled.div`
inset 2px 2px 0 0 #dfdfdf;
color: ${theme.colors.primary.text[100]};
display: flex;
- font-family: ${theme.font.family.mono};
+ font-family: ${theme.font.family.retro};
font-size: ${theme.font.size(4)};
justify-content: center;
line-height: ${theme.spacing(4)};
min-height: ${theme.spacing(10)};
padding: ${theme.spacing(1.5)} ${theme.spacing(4.5)};
- text-transform: uppercase;
+ text-decoration: none;
width: 100%;
`;
+const Separator = styled.div`
+ border-top: 1px solid ${theme.colors.secondary.border[100]};
+ width: 100%;
+`;
+
+const FooterNote = styled.p`
+ color: ${theme.colors.primary.text[60]};
+ font-family: ${theme.font.family.retro};
+ font-size: ${theme.font.size(4.5)};
+ line-height: ${theme.spacing(5.5)};
+ margin: 0;
+`;
+
const SectionLabel = styled.p`
color: ${theme.colors.primary.text[80]};
- font-family: ${theme.font.family.mono};
+ font-family: ${theme.font.family.retro};
font-size: ${theme.font.size(5)};
line-height: ${theme.spacing(6)};
margin: 0;
@@ -124,14 +306,14 @@ const SectionLabel = styled.p`
const AddonRow = styled.div`
align-items: flex-start;
- display: flex;
- justify-content: space-between;
+ display: grid;
gap: ${theme.spacing(4)};
+ grid-template-columns: minmax(0, 1fr) minmax(148px, 220px);
width: 100%;
`;
const CheckboxLabel = styled.label<{ disabled?: boolean }>`
- align-items: center;
+ align-items: flex-start;
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex: 1;
@@ -150,85 +332,306 @@ const HiddenCheckbox = styled.input`
`;
const CheckboxFace = styled.span<{ checked: boolean }>`
+ aspect-ratio: 1 / 1;
background-color: ${({ checked }) =>
- checked ? theme.colors.secondary.background[100] : 'rgba(255, 255, 255, 0.05)'};
+ checked ? theme.colors.primary.background[100] : 'rgba(255, 255, 255, 0.05)'};
+ border-radius: 0;
+ box-sizing: border-box;
box-shadow:
inset -1.5px -1.5px 0 0 #ffffff,
inset 1.5px 1.5px 0 0 #808080,
inset -3px -3px 0 0 #dfdfdf,
inset 3px 3px 0 0 #0a0a0a;
+ display: block;
flex-shrink: 0;
- height: ${theme.spacing(5)};
+ height: ${theme.spacing(5.5)};
position: relative;
- width: ${theme.spacing(5)};
+ transform: ${({ checked }) => (checked ? 'scale(1.08)' : 'scale(1)')};
+ transform-origin: center;
+ transition:
+ transform 140ms ease-out,
+ background-color 140ms ease-out;
+ width: ${theme.spacing(5.5)};
`;
const CheckGlyph = styled.span`
color: ${theme.colors.primary.text[100]};
font-family: ${theme.font.family.mono};
- font-size: ${theme.font.size(3)};
+ font-size: ${theme.font.size(4)};
left: 50%;
line-height: 1;
position: absolute;
top: 50%;
- transform: translate(-50%, -58%);
+ transform: translate(-50%, -55%);
`;
const AddonLabelText = styled.span`
color: ${theme.colors.primary.text[60]};
- font-family: ${theme.font.family.mono};
+ font-family: ${theme.font.family.retro};
font-size: ${theme.font.size(4.5)};
line-height: ${theme.spacing(5.5)};
- white-space: nowrap;
+ white-space: pre-line;
`;
const AddonRightText = styled.span`
- color: ${theme.colors.primary.text[100]};
- flex-shrink: 0;
- font-family: ${theme.font.family.mono};
+ display: block;
+ font-family: ${theme.font.family.retro};
font-size: ${theme.font.size(4.5)};
line-height: ${theme.spacing(5.5)};
text-align: right;
- white-space: nowrap;
`;
+const AddonRightLine = styled.span<{ muted?: boolean }>`
+ color: ${({ muted }) =>
+ muted ? theme.colors.primary.text[60] : theme.colors.primary.text[100]};
+ display: block;
+`;
+
+const AddonRightPart = styled.span`
+ color: inherit;
+ font: inherit;
+`;
+
+const renderRightLabelParts = (lines: SalesforceRichTextPartType[][]) =>
+ lines.map((line, lineIndex) => (
+ 0}>
+ {line.map((part, partIndex) => (
+
+ {part.text}
+
+ ))}
+
+ ));
+
+const renderRightLabel = (label: string) =>
+ label.split('\n').map((line, lineIndex) => (
+ 0}>
+ {line}
+
+ ));
+
export type PricingWindowProps = {
checkedIds: ReadonlySet;
- onAddonToggle: (addon: SalesforceAddonRowType) => void;
+ onAddonToggle: (
+ addon: SalesforceAddonRowType,
+ anchorRect: DOMRect | null,
+ ) => void;
+ onClose: () => void;
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}
-
+
+
+
+ {pricing.windowTitle}
+
+ undefined}
+ type="button"
+ >
+ ?
+
+
+ ×
+
+
+
+
+
+
+
+
+ {pricing.productTitle}
+
+ {formatPriceAmount(perSeatPriceAmount)}
+ {pricing.priceSuffix}
+
+ {fixedPriceAmount > 0 ? (
+
+
+ {formatPriceAmount(totalPriceAmount)}
+
+ {pricing.totalPriceLabel}
+
+ ) : null}
+
+
+
+
+
+
+
+
-
- {pricing.productTitle}
-
- {pricing.priceAmount}
- {pricing.priceSuffix}
-
-
- {pricing.primaryCtaLabel}
{pricing.featureSectionHeading}
{pricing.addons.map((addon) => {
const checked = checkedIds.has(addon.id);
return (
-
+ {
+ addonAnchorRefs.current[addon.id] = node;
+ }}
+ >
onAddonToggle(addon)}
+ onChange={() =>
+ onAddonToggle(
+ addon,
+ addonAnchorRefs.current[addon.id]?.getBoundingClientRect() ??
+ null,
+ )
+ }
type="checkbox"
/>
@@ -236,11 +639,24 @@ export function PricingWindow({
{addon.label}
- {addon.rightLabel}
+
+ {addon.rightLabelParts
+ ? renderRightLabelParts(addon.rightLabelParts)
+ : renderRightLabel(addon.rightLabel)}
+
);
})}
- {pricing.secondaryCtaLabel}
+ {pricing.secondaryCtaNote ? (
+ {pricing.secondaryCtaNote}
+ ) : null}
+
+ {pricing.secondaryCtaLabel}
+
diff --git a/packages/twenty-website-new/src/sections/Salesforce/components/WrongChoicePopup/WrongChoicePopup.tsx b/packages/twenty-website-new/src/sections/Salesforce/components/WrongChoicePopup/WrongChoicePopup.tsx
index 6ccb2a060e..c5c6657d02 100644
--- a/packages/twenty-website-new/src/sections/Salesforce/components/WrongChoicePopup/WrongChoicePopup.tsx
+++ b/packages/twenty-website-new/src/sections/Salesforce/components/WrongChoicePopup/WrongChoicePopup.tsx
@@ -2,10 +2,18 @@
import { theme } from '@/theme';
import { styled } from '@linaria/react';
+import { useEffect, useState } from 'react';
-const POPUP_WIDTH = 321;
+export const WRONG_CHOICE_POPUP_WIDTH = 321;
+const POPUP_VISIBLE_DURATION_MS = 3000;
+const POPUP_FADE_DURATION_MS = 240;
-const Shell = styled.div<{ stackIndex: number }>`
+const Shell = styled.div<{
+ isClosing: boolean;
+ layerIndex: number;
+ left: number;
+ top: number;
+}>`
background-color: #c0c0c0;
box-shadow:
inset -1px -1px 0 0 #0a0a0a,
@@ -14,17 +22,20 @@ const Shell = styled.div<{ stackIndex: number }>`
inset 2px 2px 0 0 #ffffff;
display: flex;
flex-direction: column;
- left: ${({ stackIndex }) => 24 + stackIndex * 18}px;
+ left: ${({ left }) => left}px;
+ opacity: ${({ isClosing }) => (isClosing ? 0 : 1)};
padding: 3px;
+ pointer-events: ${({ isClosing }) => (isClosing ? 'none' : 'auto')};
position: absolute;
- top: ${({ stackIndex }) => 120 + stackIndex * 20}px;
- width: ${POPUP_WIDTH}px;
- z-index: ${({ stackIndex }) => 20 + stackIndex};
+ top: ${({ top }) => top}px;
+ transition: opacity ${POPUP_FADE_DURATION_MS}ms ease-out;
+ width: ${WRONG_CHOICE_POPUP_WIDTH}px;
+ z-index: ${({ layerIndex }) => 20 + layerIndex};
`;
const TitleBar = styled.div`
align-items: center;
- background: linear-gradient(90deg, #c00000 0%, #b5b5b5 100%);
+ background: linear-gradient(90deg, #008000 0%, #b5b5b5 100%);
display: flex;
justify-content: space-between;
padding: 3px 2px 3px 3px;
@@ -32,8 +43,8 @@ const TitleBar = styled.div`
`;
const TitleText = styled.p`
- color: ${theme.colors.secondary.background[100]};
- font-family: ${theme.font.family.mono};
+ color: ${theme.colors.secondary.text[100]};
+ font-family: ${theme.font.family.retro};
font-size: ${theme.font.size(4)};
line-height: 12px;
margin: 0;
@@ -48,6 +59,9 @@ const CloseButton = styled.button`
inset -2px -2px 0 0 #808080,
inset 2px 2px 0 0 #dfdfdf;
cursor: pointer;
+ font-family: ${theme.font.family.retro};
+ font-size: ${theme.font.size(4)};
+ line-height: 1;
padding: 3px 4px 4px;
position: relative;
@@ -67,16 +81,16 @@ const BodyRow = styled.div`
`;
const IconMark = styled.span`
- color: #c00000;
+ color: #008000;
flex-shrink: 0;
- font-family: ${theme.font.family.mono};
+ font-family: ${theme.font.family.retro};
font-size: ${theme.font.size(8)};
line-height: 1;
`;
const BodyText = styled.p`
color: ${theme.colors.primary.text[80]};
- font-family: ${theme.font.family.mono};
+ font-family: ${theme.font.family.retro};
font-size: ${theme.font.size(5)};
line-height: ${theme.spacing(6)};
margin: 0;
@@ -84,30 +98,71 @@ const BodyText = styled.p`
export type WrongChoicePopupProps = {
body: string;
+ isClosingRequested?: boolean;
+ layerIndex: number;
+ left: number;
onClose: () => void;
- stackIndex: number;
+ top: number;
titleBar: string;
titleId: string;
};
export function WrongChoicePopup({
body,
+ isClosingRequested = false,
+ layerIndex,
+ left,
onClose,
- stackIndex,
+ top,
titleBar,
titleId,
}: WrongChoicePopupProps) {
+ const [isClosing, setIsClosing] = useState(false);
+
+ useEffect(() => {
+ const fadeTimer = window.setTimeout(() => {
+ setIsClosing(true);
+ }, POPUP_VISIBLE_DURATION_MS);
+
+ return () => {
+ window.clearTimeout(fadeTimer);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (isClosingRequested) {
+ setIsClosing(true);
+ }
+ }, [isClosingRequested]);
+
+ useEffect(() => {
+ if (!isClosing) {
+ return;
+ }
+
+ const removeTimer = window.setTimeout(() => {
+ onClose();
+ }, POPUP_FADE_DURATION_MS);
+
+ return () => {
+ window.clearTimeout(removeTimer);
+ };
+ }, [isClosing, onClose]);
+
return (
{titleBar}
undefined}
type="button"
>
×
diff --git a/packages/twenty-website-new/src/sections/Salesforce/constants/index.ts b/packages/twenty-website-new/src/sections/Salesforce/constants/index.ts
deleted file mode 100644
index c5ed2d71ba..0000000000
--- a/packages/twenty-website-new/src/sections/Salesforce/constants/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export {
- SALESFORCE_POPUPS,
- type SalesforcePopupKey,
- type SalesforceWrongChoicePopupType,
-} from './salesforce-popups';
diff --git a/packages/twenty-website-new/src/sections/Salesforce/constants/salesforce-popups.ts b/packages/twenty-website-new/src/sections/Salesforce/constants/salesforce-popups.ts
deleted file mode 100644
index 3feb5751ad..0000000000
--- a/packages/twenty-website-new/src/sections/Salesforce/constants/salesforce-popups.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export type SalesforceWrongChoicePopupType = {
- body: string;
- titleBar: string;
-};
-
-export const SALESFORCE_POPUPS = {
- wrongChoiceDefault: {
- body: 'Better than liquid glass !',
- titleBar: 'Wrong choice',
- },
- wrongChoiceLorem: {
- body: 'Lorem ipsum dolor sit amet',
- titleBar: 'Wrong choice',
- },
-} as const satisfies Record;
-
-export type SalesforcePopupKey = keyof typeof SALESFORCE_POPUPS;
diff --git a/packages/twenty-website-new/src/sections/Salesforce/types/SalesforceData.ts b/packages/twenty-website-new/src/sections/Salesforce/types/SalesforceData.ts
index 05f16a3992..eadfc2b5d1 100644
--- a/packages/twenty-website-new/src/sections/Salesforce/types/SalesforceData.ts
+++ b/packages/twenty-website-new/src/sections/Salesforce/types/SalesforceData.ts
@@ -1,24 +1,42 @@
import type { BodyType } from '@/design-system/components/Body/types/Body';
import type { HeadingType } from '@/design-system/components/Heading/types/Heading';
-import type { SalesforcePopupKey } from '@/sections/Salesforce/constants';
+
+export type SalesforceWrongChoicePopupType = {
+ body: string;
+ titleBar: string;
+};
+
+export type SalesforceRichTextPartType = {
+ strike?: boolean;
+ text: string;
+};
export type SalesforceAddonRowType = {
+ cost: number;
defaultChecked?: boolean;
disabled?: boolean;
+ fixedCost?: number;
id: string;
label: string;
- popupKey: SalesforcePopupKey;
+ netSpendRate?: number;
+ popup: SalesforceWrongChoicePopupType;
+ rightLabelParts?: SalesforceRichTextPartType[][];
rightLabel: string;
+ sharedCostKey?: string;
};
export type SalesforcePricingPanelType = {
+ basePriceAmount: number;
+ productIconAlt: string;
+ productIconSrc: string;
+ totalPriceLabel: string;
windowTitle: string;
productTitle: string;
- priceAmount: string;
priceSuffix: string;
- primaryCtaLabel: string;
featureSectionHeading: string;
addons: SalesforceAddonRowType[];
+ secondaryCtaNote?: string;
+ secondaryCtaHref: string;
secondaryCtaLabel: string;
};
diff --git a/packages/twenty-website-new/src/sections/Salesforce/types/index.ts b/packages/twenty-website-new/src/sections/Salesforce/types/index.ts
index 987a1c5be7..211598f358 100644
--- a/packages/twenty-website-new/src/sections/Salesforce/types/index.ts
+++ b/packages/twenty-website-new/src/sections/Salesforce/types/index.ts
@@ -2,9 +2,6 @@ export type {
SalesforceAddonRowType,
SalesforceDataType,
SalesforcePricingPanelType,
-} from './SalesforceData';
-
-export type {
- SalesforcePopupKey,
+ SalesforceRichTextPartType,
SalesforceWrongChoicePopupType,
-} from '../constants';
+} from './SalesforceData';
diff --git a/packages/twenty-website-new/src/theme/font.ts b/packages/twenty-website-new/src/theme/font.ts
index 67f39a8c4c..eb80de47e0 100644
--- a/packages/twenty-website-new/src/theme/font.ts
+++ b/packages/twenty-website-new/src/theme/font.ts
@@ -3,6 +3,7 @@ export const font = {
sans: 'var(--font-sans), sans-serif',
serif: 'var(--font-serif), serif',
mono: 'var(--font-mono), monospace',
+ retro: 'var(--font-retro), monospace',
},
weight: { light: '300', regular: '400', medium: '500' },
size: (multiplier: number) => `calc(var(--font-base) * ${multiplier})`,