From 87bb2f94bbf1163372c7c7ce622d742e8c22e0bb Mon Sep 17 00:00:00 2001 From: Thomas des Francs Date: Mon, 13 Apr 2026 15:55:38 +0200 Subject: [PATCH] Fixes on website (#19625) ## Summary - fix the halftone studio image-switch behavior so image mode uses a sane default preview distance instead of rendering nearly off-screen - add shared preview-distance handling for shape and image modes, and tune the default 3D idle auto-rotate speed - update halftone controls/export plumbing to support the latest studio settings changes - refresh website UI/content in pricing, Salesforce, menu, and billing-related sections ## Testing - Ran targeted Jest tests for halftone state and footprint logic - Ran TypeScript check for `packages/twenty-website-new` - Broader app-level/manual testing not run --- .../halftone/_components/ControlsPanel.tsx | 3 + .../halftone/_components/HalftoneCanvas.tsx | 28 +- .../halftone/_components/HalftoneStudio.tsx | 19 +- .../_components/controls/AnimationsTab.tsx | 13 + .../_components/controls/ExportTab.tsx | 3 + .../src/app/halftone/_lib/exporters.test.ts | 26 +- .../src/app/halftone/_lib/exporters.ts | 20 +- .../src/app/halftone/_lib/state.test.ts | 21 ++ .../src/app/halftone/_lib/state.ts | 14 +- .../src/app/pricing/_constants/salesforce.ts | 47 ++- .../Menu/components/Drawer/Drawer.tsx | 27 +- .../src/sections/Menu/components/Nav/Nav.tsx | 23 ++ .../BillingToggle/BillingToggle.tsx | 2 +- .../Salesforce/components/Flow/Flow.tsx | 109 +++++- .../PricingWindow/PricingWindow.tsx | 313 ++++++++++++++++-- .../Salesforce/types/SalesforceData.ts | 7 + 16 files changed, 609 insertions(+), 66 deletions(-) diff --git a/packages/twenty-website-new/src/app/halftone/_components/ControlsPanel.tsx b/packages/twenty-website-new/src/app/halftone/_components/ControlsPanel.tsx index b7dabd2b12..ba63b2ef2c 100644 --- a/packages/twenty-website-new/src/app/halftone/_components/ControlsPanel.tsx +++ b/packages/twenty-website-new/src/app/halftone/_components/ControlsPanel.tsx @@ -26,6 +26,7 @@ type ControlsPanelProps = { onBackgroundChange: (value: Partial) => void; onCopyShareLink: () => void; onDashColorChange: (value: string) => void; + onHoverDashColorChange: (value: string) => void; onExportHalftoneImage: (width: number, height: number) => void; onExportBackgroundChange: (value: boolean) => void; onExportHtml: () => void; @@ -121,6 +122,7 @@ export function ControlsPanel({ onBackgroundChange, onCopyShareLink, onDashColorChange, + onHoverDashColorChange, onExportHalftoneImage, onExportBackgroundChange, onExportHtml, @@ -207,6 +209,7 @@ export function ControlsPanel({ {visible && activeTab === 'animations' ? ( ) : null} diff --git a/packages/twenty-website-new/src/app/halftone/_components/HalftoneCanvas.tsx b/packages/twenty-website-new/src/app/halftone/_components/HalftoneCanvas.tsx index b58a8b3f10..532ef0f04d 100644 --- a/packages/twenty-website-new/src/app/halftone/_components/HalftoneCanvas.tsx +++ b/packages/twenty-website-new/src/app/halftone/_components/HalftoneCanvas.tsx @@ -112,6 +112,7 @@ const halftoneFragmentShader = ` uniform float s_3; uniform float s_4; uniform vec3 dashColor; + uniform vec3 hoverDashColor; uniform float time; uniform float waveAmount; uniform float waveSpeed; @@ -119,6 +120,7 @@ const halftoneFragmentShader = ` uniform vec2 interactionUv; uniform vec2 interactionVelocity; uniform vec2 dragOffset; + uniform float hoverHalftoneActive; uniform float hoverHalftonePowerShift; uniform float hoverHalftoneRadius; uniform float hoverHalftoneWidthShift; @@ -178,10 +180,7 @@ const halftoneFragmentShader = ` } float hoverHalftoneMask = 0.0; - if ( - abs(hoverHalftonePowerShift) > 0.0001 || - abs(hoverHalftoneWidthShift) > 0.0001 - ) { + if (hoverHalftoneActive > 0.0) { float hoverHalftoneRadiusPx = hoverHalftoneRadius * logicalResolution.y; hoverHalftoneMask = smoothstep(hoverHalftoneRadiusPx, 0.0, fragDist); } @@ -249,7 +248,8 @@ const halftoneFragmentShader = ` alpha = (1.0 - smoothstep(0.0, edge, signedDistance)) * mask; } - vec3 color = dashColor * alpha; + vec3 activeDashColor = mix(dashColor, hoverDashColor, hoverHalftoneMask); + vec3 color = activeDashColor * alpha; gl_FragColor = vec4(color, alpha); #include @@ -491,6 +491,9 @@ function updateHalftone( (resources.halftoneMaterial.uniforms.dashColor.value as THREE.Color).set( settings.halftone.dashColor, ); + (resources.halftoneMaterial.uniforms.hoverDashColor.value as THREE.Color).set( + settings.halftone.hoverDashColor, + ); resources.halftoneMaterial.uniforms.waveAmount.value = settings.animation.waveEnabled && settings.sourceMode !== 'image' ? settings.animation.waveAmount @@ -665,6 +668,11 @@ export function HalftoneCanvas({ } const texture = new THREE.Texture(imageElement); + texture.wrapS = THREE.ClampToEdgeWrapping; + texture.wrapT = THREE.ClampToEdgeWrapping; + texture.generateMipmaps = false; + texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.LinearFilter; texture.needsUpdate = true; texture.colorSpace = THREE.SRGBColorSpace; resources.imageTexture = texture; @@ -860,6 +868,9 @@ export function HalftoneCanvas({ dashColor: { value: new THREE.Color(initialSettings.halftone.dashColor), }, + hoverDashColor: { + value: new THREE.Color(initialSettings.halftone.hoverDashColor), + }, time: { value: 0 }, waveAmount: { value: 0 }, waveSpeed: { value: 1 }, @@ -867,6 +878,7 @@ export function HalftoneCanvas({ interactionUv: { value: new THREE.Vector2(0.5, 0.5) }, interactionVelocity: { value: new THREE.Vector2(0, 0) }, dragOffset: { value: new THREE.Vector2(0, 0) }, + hoverHalftoneActive: { value: 0 }, hoverHalftonePowerShift: { value: 0 }, hoverHalftoneRadius: { value: 0.2 }, hoverHalftoneWidthShift: { value: 0 }, @@ -1054,6 +1066,7 @@ export function HalftoneCanvas({ snapshotWidth, snapshotHeight, ); + halftoneMaterial.uniforms.hoverHalftoneActive.value = 0; halftoneMaterial.uniforms.hoverHalftonePowerShift.value = 0; halftoneMaterial.uniforms.hoverHalftoneWidthShift.value = 0; halftoneMaterial.uniforms.hoverLightStrength.value = 0; @@ -1577,6 +1590,10 @@ export function HalftoneCanvas({ -interaction.pointerVelocityY * logicalHeight, ); halftoneMaterial.uniforms.dragOffset.value.set(0, 0); + halftoneMaterial.uniforms.hoverHalftoneActive.value = + pointerActive && activeSettings.animation.hoverHalftoneEnabled + ? 1 + : 0; halftoneMaterial.uniforms.hoverHalftonePowerShift.value = pointerActive && activeSettings.animation.hoverHalftoneEnabled ? activeSettings.animation.hoverHalftonePowerShift @@ -1902,6 +1919,7 @@ export function HalftoneCanvas({ transmissionBacksideTarget, transmissionTarget, }); + halftoneMaterial.uniforms.hoverHalftoneActive.value = 0; halftoneMaterial.uniforms.hoverHalftonePowerShift.value = 0; halftoneMaterial.uniforms.hoverHalftoneWidthShift.value = 0; halftoneMaterial.uniforms.hoverLightStrength.value = 0; diff --git a/packages/twenty-website-new/src/app/halftone/_components/HalftoneStudio.tsx b/packages/twenty-website-new/src/app/halftone/_components/HalftoneStudio.tsx index 0d23fb8ff6..d68ae1c200 100644 --- a/packages/twenty-website-new/src/app/halftone/_components/HalftoneStudio.tsx +++ b/packages/twenty-website-new/src/app/halftone/_components/HalftoneStudio.tsx @@ -167,6 +167,7 @@ const HiddenFileInput = styled.input` display: none; `; +const DEFAULT_PREVIEW_DISTANCE = 6; const DEFAULT_IMAGE_ASSET_PATH = '/images/shared/halftone/twenty-logo.svg'; const DEFAULT_IMAGE_FILENAME = 'twenty-logo.svg'; type PendingFilePicker = { @@ -282,7 +283,9 @@ export function HalftoneStudio() { createInitialHalftoneStudioState, ); const [controlsVisible, setControlsVisible] = useState(true); - const [previewDistance, setPreviewDistance] = useState(7); + const [previewDistance, setPreviewDistance] = useState( + DEFAULT_PREVIEW_DISTANCE, + ); const [activeGeometry, setActiveGeometry] = useState( () => createFallbackGeometry(), ); @@ -566,6 +569,7 @@ export function HalftoneStudio() { const activateUploadedModel = useCallback( (file: File) => { const currentDashColor = state.settings.halftone.dashColor; + const currentHoverDashColor = state.settings.halftone.hoverDashColor; const extension = file.name.split('.').pop()?.toLowerCase(); const loader = extension === 'glb' ? 'glb' : 'fbx'; const nextShape: HalftoneGeometrySpec = { @@ -594,6 +598,7 @@ export function HalftoneStudio() { value: { ...halftoneBySourceModeReference.current.shape, dashColor: currentDashColor, + hoverDashColor: currentHoverDashColor, }, }); }, @@ -603,6 +608,7 @@ export function HalftoneStudio() { const activateUploadedImage = useCallback( (file: File) => { const currentDashColor = state.settings.halftone.dashColor; + const currentHoverDashColor = state.settings.halftone.hoverDashColor; setImageFile(file); halftoneBySourceModeReference.current[state.settings.sourceMode] = { ...state.settings.halftone, @@ -613,6 +619,7 @@ export function HalftoneStudio() { value: { ...halftoneBySourceModeReference.current.image, dashColor: currentDashColor, + hoverDashColor: currentHoverDashColor, }, }); }, @@ -788,7 +795,9 @@ export function HalftoneStudio() { exportPoseReference.current = preset.initialPose; setCanvasInitialPose(preset.initialPose); - setPreviewDistance(preset.previewDistance ?? REFERENCE_PREVIEW_DISTANCE); + const nextPreviewDistance = + preset.previewDistance ?? REFERENCE_PREVIEW_DISTANCE; + setPreviewDistance(nextPreviewDistance); setExportName( preset.componentName ?? presetFile.name.replace(/\.(tsx|html)$/i, ''), ); @@ -1100,6 +1109,12 @@ export function HalftoneStudio() { value: { dashColor: value }, }) } + onHoverDashColorChange={(value) => + dispatch({ + type: 'patchHalftone', + value: { hoverDashColor: value }, + }) + } onExportHalftoneImage={(width, height) => { void handleExportHalftoneImage(width, height); }} diff --git a/packages/twenty-website-new/src/app/halftone/_components/controls/AnimationsTab.tsx b/packages/twenty-website-new/src/app/halftone/_components/controls/AnimationsTab.tsx index 6351a9bfa5..78b9178797 100644 --- a/packages/twenty-website-new/src/app/halftone/_components/controls/AnimationsTab.tsx +++ b/packages/twenty-website-new/src/app/halftone/_components/controls/AnimationsTab.tsx @@ -7,6 +7,9 @@ import { } from '@/app/halftone/_lib/formatters'; import type { HalftoneStudioSettings } from '@/app/halftone/_lib/state'; import { + ColorControlLabel, + ColorControlRow, + ColorField, ControlGrid, LabelWithTooltip, Section, @@ -25,6 +28,7 @@ type AnimationsTabProps = { onAnimationSettingsChange: ( value: Partial, ) => void; + onHoverDashColorChange: (value: string) => void; settings: HalftoneStudioSettings; }; @@ -34,6 +38,7 @@ function effectLabel(label: string, description: string) { export function AnimationsTab({ onAnimationSettingsChange, + onHoverDashColorChange, settings, }: AnimationsTabProps) { const animation = settings.animation; @@ -108,6 +113,14 @@ export function AnimationsTab({ > Radius + + Hover color + + ) : null} diff --git a/packages/twenty-website-new/src/app/halftone/_components/controls/ExportTab.tsx b/packages/twenty-website-new/src/app/halftone/_components/controls/ExportTab.tsx index b1f53a0d5b..e23351a785 100644 --- a/packages/twenty-website-new/src/app/halftone/_components/controls/ExportTab.tsx +++ b/packages/twenty-website-new/src/app/halftone/_components/controls/ExportTab.tsx @@ -176,6 +176,9 @@ export function ExportTab({
{`// Dash color: ${settings.halftone.dashColor}`}
+
+ {`// Hover color: ${settings.halftone.hoverDashColor}`} +
{`// Scale: ${settings.halftone.scale.toFixed(2)}`}
diff --git a/packages/twenty-website-new/src/app/halftone/_lib/exporters.test.ts b/packages/twenty-website-new/src/app/halftone/_lib/exporters.test.ts index ac35047609..244214c380 100644 --- a/packages/twenty-website-new/src/app/halftone/_lib/exporters.test.ts +++ b/packages/twenty-website-new/src/app/halftone/_lib/exporters.test.ts @@ -1,9 +1,13 @@ import { generateReactComponent, + parseExportedPreset, generateStandaloneHtml, } from '@/app/halftone/_lib/exporters'; import { resolveExportArtifactNames } from '@/app/halftone/_lib/exportNames'; -import { DEFAULT_HALFTONE_SETTINGS } from '@/app/halftone/_lib/state'; +import { + DEFAULT_HALFTONE_SETTINGS, + normalizeHalftoneStudioSettings, +} from '@/app/halftone/_lib/state'; describe('halftone export naming', () => { it('normalizes free-form export names into safe component and file names', () => { @@ -34,4 +38,24 @@ describe('halftone export naming', () => { expect(output).toContain('HeroExport2026'); }); + + it('parses legacy exported presets without a hover dash color', () => { + const output = generateReactComponent( + DEFAULT_HALFTONE_SETTINGS, + undefined, + 'legacy hover color preset', + ); + const legacyOutput = output.replace( + /("dashColor": "[^"]+"),\n(\s*)"hoverDashColor": "[^"]+"/, + '$1', + ); + + const parsed = parseExportedPreset(legacyOutput); + + expect( + normalizeHalftoneStudioSettings(parsed.settings).halftone.hoverDashColor, + ).toBe( + DEFAULT_HALFTONE_SETTINGS.halftone.hoverDashColor, + ); + }); }); diff --git a/packages/twenty-website-new/src/app/halftone/_lib/exporters.ts b/packages/twenty-website-new/src/app/halftone/_lib/exporters.ts index 03085470e8..8d0800d1c6 100644 --- a/packages/twenty-website-new/src/app/halftone/_lib/exporters.ts +++ b/packages/twenty-website-new/src/app/halftone/_lib/exporters.ts @@ -102,6 +102,7 @@ const halftoneFragmentShader = ` uniform float s_3; uniform float s_4; uniform vec3 dashColor; + uniform vec3 hoverDashColor; uniform float time; uniform float waveAmount; uniform float waveSpeed; @@ -109,6 +110,7 @@ const halftoneFragmentShader = ` uniform vec2 interactionUv; uniform vec2 interactionVelocity; uniform vec2 dragOffset; + uniform float hoverHalftoneActive; uniform float hoverHalftonePowerShift; uniform float hoverHalftoneRadius; uniform float hoverHalftoneWidthShift; @@ -168,10 +170,7 @@ const halftoneFragmentShader = ` } float hoverHalftoneMask = 0.0; - if ( - abs(hoverHalftonePowerShift) > 0.0001 || - abs(hoverHalftoneWidthShift) > 0.0001 - ) { + if (hoverHalftoneActive > 0.0) { float hoverHalftoneRadiusPx = hoverHalftoneRadius * logicalResolution.y; hoverHalftoneMask = smoothstep(hoverHalftoneRadiusPx, 0.0, fragDist); } @@ -236,7 +235,8 @@ const halftoneFragmentShader = ` alpha = (1.0 - smoothstep(0.0, edge, signedDistance)) * mask; } - vec3 color = dashColor * alpha; + vec3 activeDashColor = mix(dashColor, hoverDashColor, hoverHalftoneMask); + vec3 color = activeDashColor * alpha; gl_FragColor = vec4(color, alpha); #include @@ -2020,6 +2020,9 @@ async function mountHalftoneCanvas(options) { s_3: { value: settings.halftone.power }, s_4: { value: settings.halftone.width }, dashColor: { value: new THREE.Color(settings.halftone.dashColor) }, + hoverDashColor: { + value: new THREE.Color(settings.halftone.hoverDashColor), + }, time: { value: 0 }, waveAmount: { value: 0 }, waveSpeed: { value: 1 }, @@ -2027,6 +2030,7 @@ async function mountHalftoneCanvas(options) { interactionUv: { value: new THREE.Vector2(0.5, 0.5) }, interactionVelocity: { value: new THREE.Vector2(0, 0) }, dragOffset: { value: new THREE.Vector2(0, 0) }, + hoverHalftoneActive: { value: 0 }, hoverHalftonePowerShift: { value: 0 }, hoverHalftoneRadius: { value: 0.2 }, hoverHalftoneWidthShift: { value: 0 }, @@ -2594,6 +2598,9 @@ async function mountHalftoneCanvas(options) { s_3: { value: settings.halftone.power }, s_4: { value: settings.halftone.width }, dashColor: { value: new THREE.Color(settings.halftone.dashColor) }, + hoverDashColor: { + value: new THREE.Color(settings.halftone.hoverDashColor), + }, time: { value: 0 }, waveAmount: { value: 0 }, waveSpeed: { value: settings.animation.waveSpeed }, @@ -2601,6 +2608,7 @@ async function mountHalftoneCanvas(options) { interactionUv: { value: new THREE.Vector2(0.5, 0.5) }, interactionVelocity: { value: new THREE.Vector2(0, 0) }, dragOffset: { value: new THREE.Vector2(0, 0) }, + hoverHalftoneActive: { value: 0 }, hoverHalftonePowerShift: { value: 0 }, hoverHalftoneRadius: { value: settings.animation.hoverHalftoneRadius }, hoverHalftoneWidthShift: { value: 0 }, @@ -2813,6 +2821,8 @@ async function mountHalftoneCanvas(options) { -interaction.pointerVelocityY * getVirtualHeight(), ); halftoneMaterial.uniforms.dragOffset.value.set(0, 0); + halftoneMaterial.uniforms.hoverHalftoneActive.value = + pointerActive && settings.animation.hoverHalftoneEnabled ? 1 : 0; halftoneMaterial.uniforms.hoverHalftonePowerShift.value = pointerActive && settings.animation.hoverHalftoneEnabled ? settings.animation.hoverHalftonePowerShift diff --git a/packages/twenty-website-new/src/app/halftone/_lib/state.test.ts b/packages/twenty-website-new/src/app/halftone/_lib/state.test.ts index 5470afac3e..b662134bd7 100644 --- a/packages/twenty-website-new/src/app/halftone/_lib/state.test.ts +++ b/packages/twenty-website-new/src/app/halftone/_lib/state.test.ts @@ -56,4 +56,25 @@ describe('halftone studio state defaults', () => { hoverReturn: false, }); }); + + it('backfills the hover dash color when older presets do not include it', () => { + const normalized = normalizeHalftoneStudioSettings( + JSON.parse(`{ + "sourceMode": "image", + "halftone": { + "enabled": true, + "scale": 24.72, + "power": -0.07, + "width": 0.46, + "imageContrast": 1, + "dashColor": "#112233" + } + }`) as Partial, + ); + + expect(normalized.halftone.dashColor).toBe('#112233'); + expect(normalized.halftone.hoverDashColor).toBe( + DEFAULT_HALFTONE_SETTINGS.halftone.hoverDashColor, + ); + }); }); diff --git a/packages/twenty-website-new/src/app/halftone/_lib/state.ts b/packages/twenty-website-new/src/app/halftone/_lib/state.ts index 3d4a365880..6c37b00766 100644 --- a/packages/twenty-website-new/src/app/halftone/_lib/state.ts +++ b/packages/twenty-website-new/src/app/halftone/_lib/state.ts @@ -38,6 +38,7 @@ export interface HalftoneEffectSettings { width: number; imageContrast: number; dashColor: string; + hoverDashColor: string; } export interface HalftoneBackgroundSettings { @@ -201,6 +202,7 @@ export const DEFAULT_SHAPE_HALFTONE_SETTINGS: HalftoneEffectSettings = { width: 0.46, imageContrast: 1, dashColor: '#4A38F5', + hoverDashColor: '#4A38F5', }; export const DEFAULT_IMAGE_HALFTONE_SETTINGS: HalftoneEffectSettings = { @@ -210,6 +212,7 @@ export const DEFAULT_IMAGE_HALFTONE_SETTINGS: HalftoneEffectSettings = { width: 0.46, imageContrast: 1, dashColor: '#4A38F5', + hoverDashColor: '#4A38F5', }; export const DEFAULT_SOLID_MATERIAL_SETTINGS: HalftoneMaterialSettings = { @@ -286,7 +289,7 @@ export const DEFAULT_SOLID_ANIMATION_SETTINGS: HalftoneAnimationSettings = { dragFlowEnabled: false, lightSweepEnabled: false, rotateEnabled: false, - autoSpeed: 4, + autoSpeed: 0.2, autoWobble: 0.3, breatheAmount: 0.04, breatheSpeed: 0.8, @@ -404,7 +407,9 @@ export const LEGACY_HALFTONE_SETTING_KEYS = [ export function isRoundedBandHalftoneSettings( value: unknown, -): value is HalftoneEffectSettings { +): value is Omit & { + hoverDashColor?: string; +} { if (!value || typeof value !== 'object') { return false; } @@ -417,7 +422,9 @@ export function isRoundedBandHalftoneSettings( typeof candidate.power === 'number' && typeof candidate.width === 'number' && typeof candidate.imageContrast === 'number' && - typeof candidate.dashColor === 'string' + typeof candidate.dashColor === 'string' && + (typeof candidate.hoverDashColor === 'string' || + typeof candidate.hoverDashColor === 'undefined') ); } @@ -438,6 +445,7 @@ function normalizeHalftoneEffectSettings( width: settings?.width ?? defaults.width, imageContrast: settings?.imageContrast ?? defaults.imageContrast, dashColor: settings?.dashColor ?? defaults.dashColor, + hoverDashColor: settings?.hoverDashColor ?? defaults.hoverDashColor, }; } 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 0711b4549c..7430d3ef02 100644 --- a/packages/twenty-website-new/src/app/pricing/_constants/salesforce.ts +++ b/packages/twenty-website-new/src/app/pricing/_constants/salesforce.ts @@ -4,11 +4,11 @@ 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.', + text: "Some call this enterprise pricing. We prefer a CRM where API access, webhooks, and workflows don't show up as surprise add-ons.", }, heading: [ { text: 'Trust the n°1 CRM,', fontFamily: 'serif' }, - { text: ' or not!', fontFamily: 'sans' }, + { text: ' or not !', fontFamily: 'sans' }, ], pricing: { addons: [ @@ -43,6 +43,10 @@ export const SALESFORCE_DATA: SalesforceDataType = { titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: 'Unavailable', + tooltip: { + title: 'Unavailable', + body: 'Real-time is a state of mind, not a feature.', + }, }, { cost: 0, @@ -55,13 +59,17 @@ export const SALESFORCE_DATA: SalesforceDataType = { titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: 'Retro 2015', + tooltip: { + title: 'Included!', + body: 'Better than Liquid Glass!', + }, }, { cost: 5, id: 'sso', label: 'SSO', popup: { - body: 'Secure logins cost extra. Naturally.', + body: 'Only $5 for SSO. Practically a charity program.', titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: '+$5/user per month', @@ -71,7 +79,7 @@ export const SALESFORCE_DATA: SalesforceDataType = { id: 'permissions', label: '11 permissions\ngroups', popup: { - body: 'Granular permissions live behind yet another paywall.', + body: 'Experience enterprise-grade granularity, starting with an 11th permission.', titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: '+$75/user per month\nSwitch to enterprise!', @@ -82,7 +90,7 @@ export const SALESFORCE_DATA: SalesforceDataType = { id: 'maps', label: 'Maps view', popup: { - body: 'Apparently even seeing your deals on a map is a luxury.', + body: 'Visualize your customers on a map!', titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: '+$105/user per month', @@ -92,7 +100,7 @@ export const SALESFORCE_DATA: SalesforceDataType = { id: 'workflows', label: '6 workflows', popup: { - body: 'Workflow automation stops being basic the second it becomes useful.', + body: 'Start automating at huge scale!', titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: '+$75/user per month\nSwitch to enterprise!', @@ -103,7 +111,7 @@ export const SALESFORCE_DATA: SalesforceDataType = { id: 'lock-in', label: 'Lock-in', popup: { - body: 'The discount gets better the harder it is to leave.', + body: 'They call it customer loyalty. We call it a very affectionate cage.', titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: '3 2 years contract\n-33% off', @@ -123,6 +131,10 @@ export const SALESFORCE_DATA: SalesforceDataType = { titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: 'Free for you!', + tooltip: { + title: 'Included!', + body: 'Available on YouTube!', + }, }, { cost: 0, @@ -134,6 +146,10 @@ export const SALESFORCE_DATA: SalesforceDataType = { titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: 'Out of stock', + tooltip: { + title: 'Out of stock', + body: 'Your data prefers someone else\'s servers.', + }, }, { cost: 0, @@ -146,13 +162,17 @@ export const SALESFORCE_DATA: SalesforceDataType = { titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: 'Extended run!', + tooltip: { + title: 'Included!', + body: 'Outlived every redesign since 2004.', + }, }, { cost: 75, id: 'flow-orchestration', label: 'Flow\norchestration', popup: { - body: 'Orchestration brings its own fee schedule, naturally.', + body: 'Because true orchestration means putting a dollar sign on every dramatic entrance.', titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: @@ -169,13 +189,17 @@ export const SALESFORCE_DATA: SalesforceDataType = { titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: 'Coming soon!', + tooltip: { + title: 'Coming soon!', + body: 'Pagination builds character.', + }, }, { cost: 75, id: 'ai-einstein', label: 'AI (Einstein)', popup: { - body: 'AI is available right after an enterprise-sized upgrade.', + body: 'become a genius!', titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: '+$75/user per month', @@ -186,7 +210,7 @@ export const SALESFORCE_DATA: SalesforceDataType = { label: 'Encrypt your data', netSpendRate: 0.2, popup: { - body: 'Data protection is packaged like an optional luxury.', + body: 'Because apparently privacy feels more premium with a surcharge.', titleBar: SALESFORCE_POPUP_TITLE, }, rightLabel: @@ -195,7 +219,8 @@ export const SALESFORCE_DATA: SalesforceDataType = { }, ], basePriceAmount: 100, - featureSectionHeading: 'Best for Salesforce', + promoTag: 'Best for\nSalesforce', + featureSectionHeading: 'Add-ons', productIconAlt: 'Retro help document icon', productIconSrc: '/images/pricing/salesforce/help-icon.png', priceSuffix: ' / seat / month - billed yearly', diff --git a/packages/twenty-website-new/src/sections/Menu/components/Drawer/Drawer.tsx b/packages/twenty-website-new/src/sections/Menu/components/Drawer/Drawer.tsx index d8ff317720..bfd3bdc765 100644 --- a/packages/twenty-website-new/src/sections/Menu/components/Drawer/Drawer.tsx +++ b/packages/twenty-website-new/src/sections/Menu/components/Drawer/Drawer.tsx @@ -1,3 +1,5 @@ +'use client'; + import { LinkButton } from '@/design-system/components'; import { ArrowRightUpIcon, SOCIAL_ICONS } from '@/icons'; import type { @@ -10,6 +12,7 @@ import { Drawer } from '@base-ui/react/drawer'; import { Separator } from '@base-ui/react/separator'; import { styled } from '@linaria/react'; import Link from 'next/link'; +import { usePathname } from 'next/navigation'; import React from 'react'; const StyledDrawerContent = styled.div` @@ -64,6 +67,21 @@ const NavItem = styled(Link)` color: ${theme.colors.secondary.text[100]}; } + position: relative; + + &[data-active] { + color: ${theme.colors.highlight[100]}; + &::after { + content: ''; + position: absolute; + bottom: -6px; + left: 25%; + width: 50%; + height: 1px; + background: ${theme.colors.highlight[100]}; + } + } + &:focus-visible { outline: 1px solid ${theme.colors.highlight[100]}; outline-offset: 1px; @@ -147,6 +165,7 @@ export function MenuDrawer({ scheme, socialLinks, }: MenuDrawerProps) { + const pathname = usePathname(); const buttonColor = scheme === 'primary' ? 'secondary' : 'primary'; const iconFillColor = @@ -169,7 +188,13 @@ export function MenuDrawer({ + } > {item.label} diff --git a/packages/twenty-website-new/src/sections/Menu/components/Nav/Nav.tsx b/packages/twenty-website-new/src/sections/Menu/components/Nav/Nav.tsx index 73aa6d57df..272773c606 100644 --- a/packages/twenty-website-new/src/sections/Menu/components/Nav/Nav.tsx +++ b/packages/twenty-website-new/src/sections/Menu/components/Nav/Nav.tsx @@ -1,9 +1,12 @@ +'use client'; + import type { MenuNavItemType, MenuScheme } from '@/sections/Menu/types'; import { theme } from '@/theme'; import { NavigationMenu } from '@base-ui/react/navigation-menu'; import { Separator } from '@base-ui/react/separator'; import { styled } from '@linaria/react'; import Link from 'next/link'; +import { usePathname } from 'next/navigation'; import React from 'react'; const NavList = styled(NavigationMenu.List)` @@ -41,6 +44,21 @@ const NavLink = styled(NavigationMenu.Link)` color: ${theme.colors.highlight[100]}; } + position: relative; + + &[data-active] { + color: ${theme.colors.highlight[100]}; + &::after { + content: ''; + position: absolute; + bottom: -6px; + left: 40%; + width: 20%; + height: 2px; + background: ${theme.colors.highlight[100]}; + } + } + &:focus-visible { outline: 1px solid ${theme.colors.highlight[100]}; outline-offset: 1px; @@ -66,6 +84,8 @@ type NavProps = { }; export function Nav({ navItems, scheme }: NavProps) { + const pathname = usePathname(); + return ( }> @@ -74,6 +94,9 @@ export function Nav({ navItems, scheme }: NavProps) { } > {item.label} diff --git a/packages/twenty-website-new/src/sections/Plans/components/BillingToggle/BillingToggle.tsx b/packages/twenty-website-new/src/sections/Plans/components/BillingToggle/BillingToggle.tsx index ed069e43ba..20d3ec529b 100644 --- a/packages/twenty-website-new/src/sections/Plans/components/BillingToggle/BillingToggle.tsx +++ b/packages/twenty-website-new/src/sections/Plans/components/BillingToggle/BillingToggle.tsx @@ -99,7 +99,7 @@ export function BillingToggle({ type="button" > Yearly - -20% + -25% ); 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 42bbc85205..48cfb1483c 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 @@ -17,6 +17,7 @@ import { } from '../WrongChoicePopup/WrongChoicePopup'; const CopyColumn = styled.div` + color: ${theme.colors.primary.text[100]}; display: flex; flex-direction: column; gap: ${theme.spacing(2)}; @@ -28,6 +29,10 @@ const CopyColumn = styled.div` } `; +const DescriptionBody = styled(Body)` + color: ${theme.colors.primary.text[80]}; +`; + const RightColumn = styled.div` max-width: 672px; min-width: 0; @@ -49,6 +54,7 @@ const POPUP_MARGIN = 12; const POPUP_X_OFFSET = 32; const POPUP_Y_OFFSET = 12; const POPUP_STACK_OFFSET = 14; +const ESTIMATED_POPUP_HEIGHT = 116; const getPopupPosition = ( anchorRect: DOMRect | null, @@ -88,6 +94,51 @@ const getPopupPosition = ( }; }; +const clamp = (value: number, min: number, max: number) => + Math.min(Math.max(value, min), max); + +const getScatteredPopupPosition = ( + containerRect: DOMRect | null, + popupIndex: number, +) => { + if (!containerRect) { + return { + left: 24 + popupIndex * POPUP_STACK_OFFSET, + top: 120 + popupIndex * POPUP_STACK_OFFSET, + }; + } + + const maxLeft = Math.max( + POPUP_MARGIN, + containerRect.width - WRONG_CHOICE_POPUP_WIDTH - POPUP_MARGIN, + ); + const maxTop = Math.max( + POPUP_MARGIN, + containerRect.height - ESTIMATED_POPUP_HEIGHT - POPUP_MARGIN, + ); + + const horizontalRange = Math.max(0, maxLeft - POPUP_MARGIN); + const verticalRange = Math.max(0, maxTop - POPUP_MARGIN); + + // Low-discrepancy scatter so bulk popups feel sprayed across the window. + const xSeed = (popupIndex * 0.61803398875 + 0.21) % 1; + const ySeed = (popupIndex * 0.38196601125 + 0.47) % 1; + const offset = popupIndex % 2 === 0 ? POPUP_STACK_OFFSET : -POPUP_STACK_OFFSET; + + return { + left: clamp( + POPUP_MARGIN + horizontalRange * xSeed + offset, + POPUP_MARGIN, + maxLeft, + ), + top: clamp( + POPUP_MARGIN + verticalRange * ySeed + offset * 0.5, + POPUP_MARGIN, + maxTop, + ), + }; +}; + type FlowProps = SalesforceDataType & { backgroundColor: string; }; @@ -169,6 +220,54 @@ export function Flow({ backgroundColor, body, heading, pricing }: FlowProps) { setPopups((previous) => previous.filter((popup) => popup.key !== key)); }, []); + const handleSelectAll = useCallback(() => { + const enabledAddons = pricing.addons.filter((addon) => !addon.disabled); + const allChecked = enabledAddons.every((addon) => checkedIds.has(addon.id)); + + if (allChecked) { + setCheckedIds((previous) => { + const next = new Set(previous); + for (const addon of enabledAddons) { + next.delete(addon.id); + } + return next; + }); + setPopups([]); + return; + } + + setCheckedIds((previous) => { + const next = new Set(previous); + for (const addon of enabledAddons) { + next.add(addon.id); + } + return next; + }); + + const containerRect = rightColumnRef.current?.getBoundingClientRect() ?? null; + const popupSequenceStart = popupSequenceRef.current; + popupSequenceRef.current += enabledAddons.length; + + setPopups( + enabledAddons.map((addon, popupIndex) => { + const popupPosition = getScatteredPopupPosition( + containerRect, + popupIndex, + ); + + return { + body: addon.popup.body, + key: `${addon.id}-${popupSequenceStart + popupIndex}`, + layerIndex: popupSequenceStart + popupIndex, + left: popupPosition.left, + sourceId: addon.id, + top: popupPosition.top, + titleBar: addon.popup.titleBar, + }; + }), + ); + }, [pricing.addons, checkedIds]); + const handleClosePricingWindow = useCallback(() => { setIsPricingWindowVisible(false); setPopups([]); @@ -177,8 +276,13 @@ export function Flow({ backgroundColor, body, heading, pricing }: FlowProps) { return ( - - + + {isPricingWindowVisible ? ( @@ -186,6 +290,7 @@ export function Flow({ backgroundColor, body, heading, pricing }: FlowProps) { checkedIds={checkedIds} onAddonToggle={handleAddonToggle} onClose={handleClosePricingWindow} + onSelectAll={handleSelectAll} pricing={pricing} /> ) : 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 f10016af65..786f83abac 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 { useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; const formatPriceAmount = (amount: number) => `$${new Intl.NumberFormat('en-US').format(amount)}`; @@ -53,27 +53,123 @@ const calculatePriceAmounts = ( }; }; +const ANIMATION_DURATION_MS = 500; + +const useAnimatedNumber = (target: number) => { + const [display, setDisplay] = useState(target); + const prevRef = useRef(target); + + useEffect(() => { + const from = prevRef.current; + prevRef.current = target; + + if (from === target) { + return; + } + + const start = performance.now(); + let rafId: number; + + const tick = (now: number) => { + const progress = Math.min((now - start) / ANIMATION_DURATION_MS, 1); + const eased = 1 - (1 - progress) ** 3; + setDisplay(Math.round(from + (target - from) * eased)); + + if (progress < 1) { + rafId = requestAnimationFrame(tick); + } + }; + + rafId = requestAnimationFrame(tick); + return () => cancelAnimationFrame(rafId); + }, [target]); + + return display; +}; + const PANEL_BACKGROUND = '#c9c9c9'; +const SALESFORCE_BLUE = '#009EDB'; + +const PanelWrapper = styled.div` + max-width: 672px; + padding-top: ${theme.spacing(8)}; + position: relative; + width: 100%; +`; + +// 16-point starburst — shallow spikes to keep text readable +const STARBURST_CLIP = `polygon( + 50% 0%, 55% 18%, 65% 3%, 66% 22%, + 80% 10%, 76% 28%, 93% 22%, 83% 36%, + 100% 42%, 86% 48%, 98% 62%, 83% 62%, + 92% 78%, 78% 72%, 76% 90%, 64% 78%, + 56% 97%, 50% 80%, 38% 100%, 36% 80%, + 22% 92%, 26% 74%, 8% 80%, 18% 64%, + 0% 58%, 16% 50%, 2% 36%, 18% 34%, + 6% 18%, 22% 26%, 20% 8%, 34% 22%, + 40% 2%, 44% 20% +)`; + +const PromoTagBorder = styled.div` + clip-path: ${STARBURST_CLIP}; + background: #005fb2; + filter: drop-shadow(2px 3px 6px rgba(0, 40, 80, 0.45)); + height: 130px; + position: absolute; + right: ${theme.spacing(25)}; + top: -16px; + transform: rotate(-8deg); + width: 200px; + z-index: 30; +`; + +const PromoTagInner = styled.div` + align-items: center; + background: ${SALESFORCE_BLUE}; + clip-path: ${STARBURST_CLIP}; + color: #ffffff; + display: flex; + font-family: ${theme.font.family.retro}; + font-size: ${theme.font.size(4.5)}; + font-weight: bold; + height: calc(100% - 8px); + justify-content: center; + left: 4px; + letter-spacing: 1px; + line-height: 1.3; + position: absolute; + text-align: center; + text-transform: uppercase; + top: 4px; + white-space: pre-line; + width: calc(100% - 8px); +`; const Panel = styled.div` background-color: ${PANEL_BACKGROUND}; display: flex; flex-direction: column; - max-width: 672px; padding: 3px; position: relative; width: 100%; + + &::after { + background: repeating-linear-gradient( + 0deg, + transparent 0px, + transparent 2px, + rgba(0, 0, 0, 0.03) 2px, + rgba(0, 0, 0, 0.03) 4px + ); + content: ''; + inset: 0; + pointer-events: none; + position: absolute; + z-index: 10; + } `; const PricingHeader = styled.div` - 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; position: relative; width: 100%; z-index: 20; @@ -161,7 +257,7 @@ const SummaryPad = styled.div` const SummaryInner = styled.div` display: flex; flex-direction: column; - gap: ${theme.spacing(4)}; + gap: ${theme.spacing(3)}; padding: ${theme.spacing(4)} ${theme.spacing(4)} 0; width: 100%; `; @@ -186,15 +282,15 @@ const ProductCopy = styled.div` display: flex; flex: 1 1 auto; flex-direction: column; - gap: ${theme.spacing(4)}; + gap: ${theme.spacing(2)}; max-width: 427px; min-width: 0; `; const ProductTitle = styled.p` color: ${theme.colors.primary.text[100]}; - font-size: ${theme.font.size(14.5)}; - line-height: ${theme.spacing(14)}; + font-size: ${theme.font.size(10)}; + line-height: ${theme.spacing(9.5)}; margin: 0; `; @@ -212,6 +308,14 @@ const PriceAmount = styled.span` line-height: ${theme.spacing(10)}; `; +const BasePriceAmount = styled.span` + color: ${theme.colors.primary.text[40]}; + font-size: ${theme.font.size(8)}; + line-height: ${theme.spacing(10)}; + text-decoration: line-through; + text-decoration-thickness: 2px; +`; + const PriceSuffix = styled.span` color: ${theme.colors.primary.text[60]}; font-size: ${theme.font.size(4.5)}; @@ -248,7 +352,7 @@ const ProductIcon = styled.img` const FakeButton = styled.a` align-items: center; - background-color: rgba(28, 28, 28, 0.2); + background-color: ${PANEL_BACKGROUND}; box-shadow: inset -1px -1px 0 0 #0a0a0a, inset 1px 1px 0 0 #ffffff, @@ -262,8 +366,10 @@ const FakeButton = styled.a` line-height: ${theme.spacing(4)}; min-height: ${theme.spacing(10)}; padding: ${theme.spacing(1.5)} ${theme.spacing(4.5)}; + position: relative; text-decoration: none; width: 100%; + z-index: 11; `; const Separator = styled.div` @@ -274,11 +380,27 @@ const Separator = styled.div` 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)}; + font-size: ${theme.font.size(5)}; + line-height: ${theme.spacing(6)}; margin: 0; `; +const FooterCtaSection = styled.div` + display: flex; + flex-direction: column; + gap: ${theme.spacing(2)}; + position: relative; + width: 100%; + z-index: 11; +`; + +const SectionHeader = styled.div` + align-items: center; + display: flex; + justify-content: space-between; + width: 100%; +`; + const SectionLabel = styled.p` color: ${theme.colors.primary.text[80]}; font-family: ${theme.font.family.retro}; @@ -289,10 +411,68 @@ const SectionLabel = styled.p` const AddonRow = styled.div` align-items: flex-start; + border-radius: 0; display: grid; gap: ${theme.spacing(4)}; grid-template-columns: minmax(0, 1fr) minmax(148px, 220px); - width: 100%; + margin: 0 -${theme.spacing(1.5)}; + padding: ${theme.spacing(1)} ${theme.spacing(1.5)}; + position: relative; + transition: background-color 0ms; + width: calc(100% + ${theme.spacing(3)}); + + &:hover { + background-color: #000080; + + span, + label span { + color: #ffffff; + } + + label span[aria-hidden] span { + color: ${theme.colors.primary.text[100]}; + } + } +`; + +const Tooltip = styled.div` + background: ${PANEL_BACKGROUND}; + 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, + 4px 4px 0 0 rgba(0, 0, 0, 0.15); + display: none; + font-family: ${theme.font.family.retro}; + left: 0; + padding: ${theme.spacing(0.5)}; + position: absolute; + top: 100%; + width: 240px; + z-index: 40; + + ${AddonRow}:hover & { + display: block; + } +`; + +const TooltipTitleBar = styled.div` + align-items: center; + background: linear-gradient(90deg, #000080 0%, #1084d0 100%); + color: #ffffff; + display: flex; + font-size: ${theme.font.size(3.5)}; + line-height: 1; + padding: 3px 4px; +`; + +const TooltipBody = styled.p` + color: ${theme.colors.primary.text[100]}; + font-size: ${theme.font.size(4)}; + line-height: 1.4; + margin: 0; + padding: ${theme.spacing(2)}; `; const CheckboxLabel = styled.label<{ disabled?: boolean }>` @@ -336,6 +516,7 @@ const CheckboxFace = styled.span<{ checked: boolean }>` transition: transform 140ms ease-out, background-color 140ms ease-out; + user-select: none; width: ${theme.spacing(5.5)}; `; @@ -345,9 +526,11 @@ const CheckGlyph = styled.span` font-size: ${theme.font.size(4)}; left: 50%; line-height: 1; + pointer-events: none; position: absolute; top: 50%; transform: translate(-50%, -55%); + user-select: none; `; const AddonLabelText = styled.span` @@ -410,6 +593,32 @@ const renderRightLabel = (label: string) => )); +const SelectAllButton = styled.button` + align-items: center; + background-color: rgba(28, 28, 28, 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[80]}; + cursor: pointer; + display: flex; + flex-shrink: 0; + font-family: ${theme.font.family.retro}; + font-size: ${theme.font.size(3.5)}; + justify-content: center; + line-height: 1; + padding: ${theme.spacing(1)} ${theme.spacing(3)}; + + &:active { + box-shadow: + inset 1px 1px 0 0 #0a0a0a, + inset -1px -1px 0 0 #ffffff; + } +`; + export type PricingWindowProps = { checkedIds: ReadonlySet; onAddonToggle: ( @@ -417,6 +626,7 @@ export type PricingWindowProps = { anchorRect: DOMRect | null, ) => void; onClose: () => void; + onSelectAll: () => void; pricing: SalesforcePricingPanelType; }; @@ -424,16 +634,26 @@ export function PricingWindow({ checkedIds, onAddonToggle, onClose, + onSelectAll, pricing, }: PricingWindowProps) { const addonAnchorRefs = useRef>({}); const { fixedPriceAmount, perSeatPriceAmount, totalPriceAmount } = calculatePriceAmounts(pricing, checkedIds); + const animatedPerSeat = useAnimatedNumber(perSeatPriceAmount); + const animatedTotal = useAnimatedNumber(totalPriceAmount); + return ( - - + ); } 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 eadfc2b5d1..73f1a0b552 100644 --- a/packages/twenty-website-new/src/sections/Salesforce/types/SalesforceData.ts +++ b/packages/twenty-website-new/src/sections/Salesforce/types/SalesforceData.ts @@ -11,6 +11,11 @@ export type SalesforceRichTextPartType = { text: string; }; +export type SalesforceAddonTooltipType = { + body: string; + title: string; +}; + export type SalesforceAddonRowType = { cost: number; defaultChecked?: boolean; @@ -23,6 +28,7 @@ export type SalesforceAddonRowType = { rightLabelParts?: SalesforceRichTextPartType[][]; rightLabel: string; sharedCostKey?: string; + tooltip?: SalesforceAddonTooltipType; }; export type SalesforcePricingPanelType = { @@ -33,6 +39,7 @@ export type SalesforcePricingPanelType = { windowTitle: string; productTitle: string; priceSuffix: string; + promoTag?: string; featureSectionHeading: string; addons: SalesforceAddonRowType[]; secondaryCtaNote?: string;