From 284ebeb12d6a46081f7da0c5fa69f86efcf48d25 Mon Sep 17 00:00:00 2001
From: "Abdullah." <125115953+mabdullahabaid@users.noreply.github.com>
Date: Fri, 8 May 2026 05:01:04 +0500
Subject: [PATCH] [Website] Reintroduce the product page. (#20349)
This PR re-introduces the Product page that we decided to hold back in
the first release. Subsequent PRs will work on polishing the Product
page to have interactive visuals, but merging the static version to
record a snapshot.
No production deployments are planned until product page is polished and
blog has the required content - we'd push to prod next week with these
two checkpoints.
---
README.md | 2 +-
packages/twenty-website-new/next.config.ts | 7 +-
.../src/app/[locale]/product/demo.data.ts | 8 ++
.../src/app/[locale]/product/page.tsx | 82 ++++++-----
.../src/app/[locale]/product/tabs.data.ts | 49 +++++++
.../why-twenty/editorial-four.data.ts | 0
.../why-twenty/editorial-one.data.ts | 0
.../why-twenty/editorial-three.data.ts | 0
.../{ => resources}/why-twenty/hero.data.ts | 0
.../why-twenty/marquee.data.ts | 0
.../{ => resources}/why-twenty/page.tsx | 12 +-
.../why-twenty/signoff.data.ts | 0
.../lib/i18n/__tests__/localize-href.test.ts | 14 +-
.../website-routing/static-website-routes.ts | 2 +-
.../src/sections/Demo/components/Cta.tsx | 16 +++
.../src/sections/Demo/components/Root.tsx | 89 ++++++++++++
.../sections/Demo/components/Screenshot.tsx | 41 ++++++
.../src/sections/Demo/components/index.ts | 5 +
.../src/sections/Footer/data.ts | 7 +-
.../src/sections/Menu/data.ts | 13 +-
.../src/sections/Tabs/components/Root.tsx | 131 ++++++++++++++++++
.../sections/Tabs/components/TabButton.tsx | 123 ++++++++++++++++
.../sections/Tabs/components/TabButtons.tsx | 52 +++++++
.../src/sections/Tabs/components/TabGroup.tsx | 44 ++++++
.../sections/Tabs/components/TabVisuals.tsx | 46 ++++++
.../src/sections/Tabs/components/index.ts | 7 +
.../src/sections/Tabs/types/index.ts | 9 ++
27 files changed, 713 insertions(+), 46 deletions(-)
create mode 100644 packages/twenty-website-new/src/app/[locale]/product/demo.data.ts
create mode 100644 packages/twenty-website-new/src/app/[locale]/product/tabs.data.ts
rename packages/twenty-website-new/src/app/[locale]/{ => resources}/why-twenty/editorial-four.data.ts (100%)
rename packages/twenty-website-new/src/app/[locale]/{ => resources}/why-twenty/editorial-one.data.ts (100%)
rename packages/twenty-website-new/src/app/[locale]/{ => resources}/why-twenty/editorial-three.data.ts (100%)
rename packages/twenty-website-new/src/app/[locale]/{ => resources}/why-twenty/hero.data.ts (100%)
rename packages/twenty-website-new/src/app/[locale]/{ => resources}/why-twenty/marquee.data.ts (100%)
rename packages/twenty-website-new/src/app/[locale]/{ => resources}/why-twenty/page.tsx (94%)
rename packages/twenty-website-new/src/app/[locale]/{ => resources}/why-twenty/signoff.data.ts (100%)
create mode 100644 packages/twenty-website-new/src/sections/Demo/components/Cta.tsx
create mode 100644 packages/twenty-website-new/src/sections/Demo/components/Root.tsx
create mode 100644 packages/twenty-website-new/src/sections/Demo/components/Screenshot.tsx
create mode 100644 packages/twenty-website-new/src/sections/Demo/components/index.ts
create mode 100644 packages/twenty-website-new/src/sections/Tabs/components/Root.tsx
create mode 100644 packages/twenty-website-new/src/sections/Tabs/components/TabButton.tsx
create mode 100644 packages/twenty-website-new/src/sections/Tabs/components/TabButtons.tsx
create mode 100644 packages/twenty-website-new/src/sections/Tabs/components/TabGroup.tsx
create mode 100644 packages/twenty-website-new/src/sections/Tabs/components/TabVisuals.tsx
create mode 100644 packages/twenty-website-new/src/sections/Tabs/components/index.ts
create mode 100644 packages/twenty-website-new/src/sections/Tabs/types/index.ts
diff --git a/README.md b/README.md
index e0f619793f..7bfc56b28a 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
-
Learn more about why we built Twenty
+
Learn more about why we built Twenty
diff --git a/packages/twenty-website-new/next.config.ts b/packages/twenty-website-new/next.config.ts
index 1ab5ac8f15..fd7b50c4a4 100644
--- a/packages/twenty-website-new/next.config.ts
+++ b/packages/twenty-website-new/next.config.ts
@@ -114,9 +114,14 @@ const nextConfig: LinariaConfig = {
destination: 'https://docs.twenty.com/twenty-ui/:slug',
permanent: true,
},
+ {
+ source: '/why-twenty',
+ destination: '/resources/why-twenty',
+ permanent: true,
+ },
{
source: '/story',
- destination: '/why-twenty',
+ destination: '/resources/why-twenty',
permanent: true,
},
{
diff --git a/packages/twenty-website-new/src/app/[locale]/product/demo.data.ts b/packages/twenty-website-new/src/app/[locale]/product/demo.data.ts
new file mode 100644
index 0000000000..053650215d
--- /dev/null
+++ b/packages/twenty-website-new/src/app/[locale]/product/demo.data.ts
@@ -0,0 +1,8 @@
+import type { ImageType } from '@/design-system/components/Image';
+
+export const DEMO_DATA: { image: ImageType } = {
+ image: {
+ src: '/images/product/demo/kanban.webp',
+ alt: '',
+ },
+};
diff --git a/packages/twenty-website-new/src/app/[locale]/product/page.tsx b/packages/twenty-website-new/src/app/[locale]/product/page.tsx
index 864850bf81..357dbb2994 100644
--- a/packages/twenty-website-new/src/app/[locale]/product/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/product/page.tsx
@@ -3,10 +3,11 @@ import { FAQ_DATA } from '@/sections/Faq/data';
import { MENU_DATA } from '@/sections/Menu/data';
import { TRUSTED_BY_DATA } from '@/sections/TrustedBy/data';
import { TalkToUsButton } from '@/lib/contact-cal';
+import { DEMO_DATA } from '@/app/[locale]/product/demo.data';
import { FEATURE_DATA } from '@/app/[locale]/product/feature.data';
import { HERO_COPY } from '@/app/[locale]/product/hero.data';
-import { SIGNOFF_COPY } from '@/app/[locale]/product/signoff.data';
import { STEPPER_DATA } from '@/app/[locale]/product/stepper.data';
+import { TABS_DATA } from '@/app/[locale]/product/tabs.data';
import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/[locale]/product/three-cards.data';
import {
Body,
@@ -27,8 +28,9 @@ import { Faq } from '@/sections/Faq/components';
import { Feature } from '@/sections/Feature/components';
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
+import { Demo } from '@/sections/Demo/components';
import { ProductStepper } from '@/sections/ProductStepper/components';
-import { Signoff } from '@/sections/Signoff/components';
+import { Tabs } from '@/sections/Tabs/components';
import { ThreeCards } from '@/sections/ThreeCards/components';
import { TrustedBy } from '@/sections/TrustedBy/components';
import { theme } from '@/theme';
@@ -113,6 +115,32 @@ export default async function ProductPage({ params }: ProductPageProps) {
/>
+
+
+
+
+ {renderText(msg`AI that actually`)}
+
+
+
+ {renderText(msg`helps you`)}
+ {' '}
+
+ {renderText(msg`work faster`)}
+
+
+
+
+
+
-
-
-
- {renderText(msg`Ready to grow`)}
-
-
-
- {renderText(msg`with`)}
- {' '}
-
- {renderText(msg`Twenty?`)}
-
-
-
+
-
+
+
+ {renderText(msg`A demo worth a`)}
+
+
+
+ {renderText(msg`thousand words`)}
+
+
+
-
-
-
+
+
+
diff --git a/packages/twenty-website-new/src/app/[locale]/product/tabs.data.ts b/packages/twenty-website-new/src/app/[locale]/product/tabs.data.ts
new file mode 100644
index 0000000000..3f56751a5a
--- /dev/null
+++ b/packages/twenty-website-new/src/app/[locale]/product/tabs.data.ts
@@ -0,0 +1,49 @@
+import { msg } from '@lingui/core/macro';
+
+import type { TabType } from '@/sections/Tabs/types';
+
+export const TABS_DATA: {
+ eyebrow: { heading: { text: ReturnType; fontFamily: 'sans' } };
+ tabs: TabType[];
+} = {
+ eyebrow: {
+ heading: {
+ text: msg`AI & Automation`,
+ fontFamily: 'sans',
+ },
+ },
+ tabs: [
+ {
+ body: msg`Show me all deals closing this month`,
+ icon: 'search',
+ image: {
+ src: '/images/product/tabs/deals.webp',
+ alt: 'Deals view',
+ },
+ },
+ {
+ body: msg`Create follow-up tasks for my top 10 accounts`,
+ icon: 'eye',
+ image: {
+ src: '/images/product/tabs/tasks.webp',
+ alt: 'Tasks view',
+ },
+ },
+ {
+ body: msg`Summarize this customer's history`,
+ icon: 'edit',
+ image: {
+ src: '/images/product/tabs/history.webp',
+ alt: 'History view',
+ },
+ },
+ {
+ body: msg`Create a workflow that send an email sequence`,
+ icon: 'check',
+ image: {
+ src: '/images/product/tabs/workflow.webp',
+ alt: 'Workflow view',
+ },
+ },
+ ],
+};
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-four.data.ts b/packages/twenty-website-new/src/app/[locale]/resources/why-twenty/editorial-four.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-four.data.ts
rename to packages/twenty-website-new/src/app/[locale]/resources/why-twenty/editorial-four.data.ts
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-one.data.ts b/packages/twenty-website-new/src/app/[locale]/resources/why-twenty/editorial-one.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-one.data.ts
rename to packages/twenty-website-new/src/app/[locale]/resources/why-twenty/editorial-one.data.ts
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-three.data.ts b/packages/twenty-website-new/src/app/[locale]/resources/why-twenty/editorial-three.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-three.data.ts
rename to packages/twenty-website-new/src/app/[locale]/resources/why-twenty/editorial-three.data.ts
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/resources/why-twenty/hero.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/[locale]/why-twenty/hero.data.ts
rename to packages/twenty-website-new/src/app/[locale]/resources/why-twenty/hero.data.ts
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/marquee.data.ts b/packages/twenty-website-new/src/app/[locale]/resources/why-twenty/marquee.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/[locale]/why-twenty/marquee.data.ts
rename to packages/twenty-website-new/src/app/[locale]/resources/why-twenty/marquee.data.ts
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/page.tsx b/packages/twenty-website-new/src/app/[locale]/resources/why-twenty/page.tsx
similarity index 94%
rename from packages/twenty-website-new/src/app/[locale]/why-twenty/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/resources/why-twenty/page.tsx
index e46aaa4cd8..a725ab11c4 100644
--- a/packages/twenty-website-new/src/app/[locale]/why-twenty/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/resources/why-twenty/page.tsx
@@ -1,11 +1,11 @@
import { msg } from '@lingui/core/macro';
import { MENU_DATA } from '@/sections/Menu/data';
-import { EDITORIAL_FOUR } from '@/app/[locale]/why-twenty/editorial-four.data';
-import { EDITORIAL_ONE } from '@/app/[locale]/why-twenty/editorial-one.data';
-import { EDITORIAL_THREE } from '@/app/[locale]/why-twenty/editorial-three.data';
-import { HERO_COPY } from '@/app/[locale]/why-twenty/hero.data';
-import { MARQUEE_DATA } from '@/app/[locale]/why-twenty/marquee.data';
-import { SIGNOFF_COPY } from '@/app/[locale]/why-twenty/signoff.data';
+import { EDITORIAL_FOUR } from '@/app/[locale]/resources/why-twenty/editorial-four.data';
+import { EDITORIAL_ONE } from '@/app/[locale]/resources/why-twenty/editorial-one.data';
+import { EDITORIAL_THREE } from '@/app/[locale]/resources/why-twenty/editorial-three.data';
+import { HERO_COPY } from '@/app/[locale]/resources/why-twenty/hero.data';
+import { MARQUEE_DATA } from '@/app/[locale]/resources/why-twenty/marquee.data';
+import { SIGNOFF_COPY } from '@/app/[locale]/resources/why-twenty/signoff.data';
import { HeadingPart, LinkButton } from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { createMessageDescriptorRenderer } from '@/lib/i18n/create-message-descriptor-renderer';
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/signoff.data.ts b/packages/twenty-website-new/src/app/[locale]/resources/why-twenty/signoff.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/[locale]/why-twenty/signoff.data.ts
rename to packages/twenty-website-new/src/app/[locale]/resources/why-twenty/signoff.data.ts
diff --git a/packages/twenty-website-new/src/lib/i18n/__tests__/localize-href.test.ts b/packages/twenty-website-new/src/lib/i18n/__tests__/localize-href.test.ts
index 34e916b19c..3dcbd49a2d 100644
--- a/packages/twenty-website-new/src/lib/i18n/__tests__/localize-href.test.ts
+++ b/packages/twenty-website-new/src/lib/i18n/__tests__/localize-href.test.ts
@@ -34,12 +34,16 @@ describe('localizeHref', () => {
});
it('strips a redundant /en prefix when targeting the default locale', () => {
- expect(localizeHref('en', '/en/why-twenty')).toBe('/why-twenty');
+ expect(localizeHref('en', '/en/resources/why-twenty')).toBe(
+ '/resources/why-twenty',
+ );
expect(localizeHref('en', '/en')).toBe('/');
});
it('strips an /en-prefixed path when targeting an unpublished locale', () => {
- expect(localizeHref('de-DE', '/en/why-twenty')).toBe('/why-twenty');
+ expect(localizeHref('de-DE', '/en/resources/why-twenty')).toBe(
+ '/resources/why-twenty',
+ );
expect(localizeHref('de-DE', '/en')).toBe('/');
});
@@ -81,7 +85,9 @@ describe('localizeHref', () => {
describe('stripLocale', () => {
it('removes a published-locale URL-segment prefix from the pathname', () => {
- expect(stripLocale('/en/why-twenty')).toBe('/why-twenty');
+ expect(stripLocale('/en/resources/why-twenty')).toBe(
+ '/resources/why-twenty',
+ );
expect(stripLocale('/fr/customers/9dots')).toBe('/customers/9dots');
});
@@ -91,7 +97,7 @@ describe('stripLocale', () => {
});
it('returns the pathname unchanged when no published locale prefix is present', () => {
- expect(stripLocale('/why-twenty')).toBe('/why-twenty');
+ expect(stripLocale('/resources/why-twenty')).toBe('/resources/why-twenty');
expect(stripLocale('/')).toBe('/');
expect(stripLocale('/fr-FR/foo')).toBe('/fr-FR/foo');
expect(stripLocale('/zh-CN')).toBe('/zh-CN');
diff --git a/packages/twenty-website-new/src/lib/website-routing/static-website-routes.ts b/packages/twenty-website-new/src/lib/website-routing/static-website-routes.ts
index be73fe438c..17547491ed 100644
--- a/packages/twenty-website-new/src/lib/website-routing/static-website-routes.ts
+++ b/packages/twenty-website-new/src/lib/website-routing/static-website-routes.ts
@@ -14,7 +14,7 @@ export const STATIC_WEBSITE_ROUTES = [
},
{
id: 'whyTwenty',
- path: '/why-twenty',
+ path: '/resources/why-twenty',
title: msg`Why Twenty — The Open Source CRM Built to Be Customised`,
description: msg`Packaged CRMs make every company look the same. Twenty is the open source CRM teams shape around their workflow, with a modern UI and a developer-first platform.`,
changeFrequency: 'monthly',
diff --git a/packages/twenty-website-new/src/sections/Demo/components/Cta.tsx b/packages/twenty-website-new/src/sections/Demo/components/Cta.tsx
new file mode 100644
index 0000000000..09c2942cd7
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/Demo/components/Cta.tsx
@@ -0,0 +1,16 @@
+import { theme } from '@/theme';
+import { styled } from '@linaria/react';
+import type { ReactNode } from 'react';
+
+const CTAsContainer = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: ${theme.spacing(4)};
+ justify-content: center;
+`;
+
+type CtaProps = { children: ReactNode };
+
+export function Cta({ children }: CtaProps) {
+ return {children};
+}
diff --git a/packages/twenty-website-new/src/sections/Demo/components/Root.tsx b/packages/twenty-website-new/src/sections/Demo/components/Root.tsx
new file mode 100644
index 0000000000..61abc60c16
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/Demo/components/Root.tsx
@@ -0,0 +1,89 @@
+import { Container } from '@/design-system/components';
+import { theme } from '@/theme';
+import { css } from '@linaria/core';
+import { styled } from '@linaria/react';
+import NextImage from 'next/image';
+import type { ReactNode } from 'react';
+
+const RootWrapper = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ position: relative;
+ width: 100%;
+`;
+
+const BackgroundLayer = styled.div`
+ align-self: stretch;
+ grid-column: 1;
+ grid-row: 1;
+ opacity: 0.6;
+ pointer-events: none;
+ position: relative;
+ width: 100%;
+ z-index: 0;
+`;
+
+const PatternLayer = styled.div`
+ bottom: 0;
+ height: 60%;
+ left: 50%;
+ position: absolute;
+ transform: translateX(-50%);
+ width: 100%;
+`;
+
+const patternImageClassName = css`
+ object-fit: cover;
+ object-position: center top;
+`;
+
+const StyledSection = styled.section`
+ grid-column: 1;
+ grid-row: 1;
+ position: relative;
+ width: 100%;
+ z-index: 1;
+`;
+
+const StyledContainer = styled(Container)`
+ display: grid;
+ grid-template-columns: 1fr;
+ justify-items: center;
+ padding-bottom: ${theme.spacing(16)};
+ padding-left: ${theme.spacing(4)};
+ padding-right: ${theme.spacing(4)};
+ padding-top: ${theme.spacing(16)};
+ row-gap: ${theme.spacing(6)};
+ text-align: center;
+ width: 100%;
+
+ @media (min-width: ${theme.breakpoints.md}px) {
+ padding-bottom: ${theme.spacing(24)};
+ padding-left: ${theme.spacing(10)};
+ padding-right: ${theme.spacing(10)};
+ padding-top: ${theme.spacing(24)};
+ }
+`;
+
+type RootProps = { children: ReactNode };
+
+export function Root({ children }: RootProps) {
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/packages/twenty-website-new/src/sections/Demo/components/Screenshot.tsx b/packages/twenty-website-new/src/sections/Demo/components/Screenshot.tsx
new file mode 100644
index 0000000000..38292aec8c
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/Demo/components/Screenshot.tsx
@@ -0,0 +1,41 @@
+import type { ImageType } from '@/design-system/components/Image';
+import { theme } from '@/theme';
+import { css } from '@linaria/core';
+import { styled } from '@linaria/react';
+import NextImage from 'next/image';
+
+const ScreenshotRoot = styled.div`
+ aspect-ratio: 136 / 69;
+ border-radius: ${theme.radius(4)};
+ margin-bottom: ${theme.spacing(11)};
+ margin-top: ${theme.spacing(12)};
+ max-width: 800px;
+ overflow: hidden;
+ position: relative;
+ width: 100%;
+
+ @media (min-width: ${theme.breakpoints.md}px) {
+ border-radius: ${theme.radius(6)};
+ margin-top: ${theme.spacing(19)};
+ }
+`;
+
+const screenshotImageClassName = css`
+ object-fit: cover;
+`;
+
+type ScreenshotProps = { image: ImageType };
+
+export function Screenshot({ image }: ScreenshotProps) {
+ return (
+
+
+
+ );
+}
diff --git a/packages/twenty-website-new/src/sections/Demo/components/index.ts b/packages/twenty-website-new/src/sections/Demo/components/index.ts
new file mode 100644
index 0000000000..15b48888ee
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/Demo/components/index.ts
@@ -0,0 +1,5 @@
+import { Cta } from './Cta';
+import { Root } from './Root';
+import { Screenshot } from './Screenshot';
+
+export const Demo = { Cta, Root, Screenshot };
diff --git a/packages/twenty-website-new/src/sections/Footer/data.ts b/packages/twenty-website-new/src/sections/Footer/data.ts
index 4532d2b48c..17fe8c8be9 100644
--- a/packages/twenty-website-new/src/sections/Footer/data.ts
+++ b/packages/twenty-website-new/src/sections/Footer/data.ts
@@ -12,9 +12,14 @@ export const FOOTER_DATA: FooterDataType = {
ctas: [],
links: [
{ label: msg`Home`, href: '/', external: false },
+ { label: msg`Product`, href: '/product', external: false },
{ label: msg`Pricing`, href: '/pricing', external: false },
{ label: msg`Partners`, href: '/partners', external: false },
- { label: msg`Why Twenty`, href: '/why-twenty', external: false },
+ {
+ label: msg`Why Twenty`,
+ href: '/resources/why-twenty',
+ external: false,
+ },
{ label: msg`Articles`, href: '/articles', external: false },
],
},
diff --git a/packages/twenty-website-new/src/sections/Menu/data.ts b/packages/twenty-website-new/src/sections/Menu/data.ts
index f42764e184..922f2b307d 100644
--- a/packages/twenty-website-new/src/sections/Menu/data.ts
+++ b/packages/twenty-website-new/src/sections/Menu/data.ts
@@ -23,7 +23,7 @@ function buildNavItems(): MenuNavItemType[] {
getLatestReleasePreview() ?? FALLBACK_RELEASES_PREVIEW;
return [
- { label: msg`Why`, href: '/why-twenty' },
+ { label: msg`Product`, href: '/product' },
{
label: msg`Resources`,
children: [
@@ -55,6 +55,17 @@ function buildNavItems(): MenuNavItemType[] {
description: msg`APIs, SDKs and webhooks to extend Twenty and ship apps on top of your CRM data.`,
},
},
+ {
+ label: msg`Why Twenty`,
+ description: msg`The open source CRM built to be customised`,
+ href: '/resources/why-twenty',
+ preview: {
+ image: '/images/why-twenty/hero/background.webp',
+ imageAlt: 'Why Twenty',
+ title: msg`Why Twenty`,
+ description: msg`Our story: building a CRM teams can truly own.`,
+ },
+ },
{
label: msg`Partners`,
description: msg`Find a Twenty partner`,
diff --git a/packages/twenty-website-new/src/sections/Tabs/components/Root.tsx b/packages/twenty-website-new/src/sections/Tabs/components/Root.tsx
new file mode 100644
index 0000000000..cee2170e9b
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/Tabs/components/Root.tsx
@@ -0,0 +1,131 @@
+import { Container } from '@/design-system/components';
+import { theme } from '@/theme';
+import { css } from '@linaria/core';
+import { styled } from '@linaria/react';
+import NextImage from 'next/image';
+import type { ReactNode } from 'react';
+
+const RootWrapper = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ position: relative;
+ width: 100%;
+`;
+
+const TabBackgrounds = styled.div`
+ align-self: stretch;
+ grid-column: 1;
+ grid-row: 1;
+ min-height: 0;
+ pointer-events: none;
+ position: relative;
+ width: 100%;
+ z-index: 0;
+`;
+
+const BackgroundShape = styled.div`
+ bottom: 0;
+ left: 0;
+ pointer-events: none;
+ position: absolute;
+ right: 0;
+ top: 0;
+`;
+
+const ShapeClip = styled.div`
+ bottom: 0;
+ left: 0;
+ overflow: hidden;
+ pointer-events: none;
+ position: absolute;
+ right: 0;
+ top: 0;
+ z-index: 0;
+`;
+
+const shapeFillClassName = css`
+ object-fit: cover;
+ object-position: center top;
+`;
+
+const PatternLayer = styled.div`
+ bottom: -81px;
+ height: 575px;
+ left: 50%;
+ opacity: 0.4;
+ pointer-events: none;
+ position: absolute;
+ transform: translateX(-50%);
+ width: 100%;
+ z-index: 1;
+`;
+
+const patternImageClassName = css`
+ object-fit: cover;
+`;
+
+const StyledSection = styled.section`
+ align-self: start;
+ grid-column: 1;
+ grid-row: 1;
+ position: relative;
+ width: 100%;
+ z-index: 2;
+`;
+
+const StyledContainer = styled(Container)`
+ color: ${theme.colors.secondary.text[100]};
+ display: grid;
+ grid-template-columns: 1fr;
+ justify-items: center;
+ padding-bottom: ${theme.spacing(16)};
+ padding-left: ${theme.spacing(4)};
+ padding-right: ${theme.spacing(4)};
+ padding-top: ${theme.spacing(16)};
+ row-gap: ${theme.spacing(6)};
+ text-align: center;
+ width: 100%;
+
+ @media (min-width: ${theme.breakpoints.md}px) {
+ padding-bottom: ${theme.spacing(40)};
+ padding-left: ${theme.spacing(10)};
+ padding-right: ${theme.spacing(10)};
+ padding-top: ${theme.spacing(40)};
+ row-gap: ${theme.spacing(6)};
+ }
+`;
+
+type RootProps = { children: ReactNode };
+
+export function Root({ children }: RootProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/packages/twenty-website-new/src/sections/Tabs/components/TabButton.tsx b/packages/twenty-website-new/src/sections/Tabs/components/TabButton.tsx
new file mode 100644
index 0000000000..af63537efc
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/Tabs/components/TabButton.tsx
@@ -0,0 +1,123 @@
+'use client';
+
+import { useRenderMessage } from '@/lib/i18n/use-render-message';
+import { INFORMATIVE_ICONS } from '@/icons';
+import type { TabType } from '@/sections/Tabs/types';
+import { theme } from '@/theme';
+import { styled } from '@linaria/react';
+
+const Label = styled.span`
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+const StyledButton = styled.button`
+ align-items: center;
+ border-radius: ${theme.radius(2)};
+ box-sizing: border-box;
+ cursor: pointer;
+ display: grid;
+ font-family: ${theme.font.family.sans};
+ font-size: ${theme.font.size(3.5)};
+ font-weight: ${theme.font.weight.regular};
+ gap: ${theme.spacing(2)};
+ grid-template-columns: minmax(0, 1fr) auto;
+ line-height: ${theme.lineHeight(3.5)};
+ max-width: 100%;
+ min-width: 0;
+ padding: ${theme.spacing(2)};
+ text-align: left;
+ transition:
+ background-color 0.2s ease,
+ background-image 0.2s ease,
+ border-color 0.2s ease,
+ color 0.2s ease;
+
+ &[data-active='true'] {
+ background-color: ${theme.colors.primary.background[100]};
+ border: 1px solid transparent;
+ color: ${theme.colors.primary.text[100]};
+ }
+
+ &[data-active='false'] {
+ background-color: ${theme.colors.secondary.background[100]};
+ background-image: linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0.1) 0%,
+ rgba(255, 255, 255, 0.1) 100%
+ );
+ border: 1px solid ${theme.colors.secondary.border[10]};
+ color: ${theme.colors.secondary.text[100]};
+ }
+
+ &:focus-visible {
+ outline: 1px solid ${theme.colors.highlight[100]};
+ outline-offset: 1px;
+ }
+`;
+
+const TabIconBox = styled.span`
+ border-radius: ${theme.radius(1)};
+ display: grid;
+ height: ${theme.spacing(6)};
+ overflow: hidden;
+ place-items: center;
+ width: ${theme.spacing(6)};
+
+ & svg {
+ display: block;
+ max-height: 100%;
+ max-width: 100%;
+ }
+
+ &[data-active='true'] {
+ background-color: ${theme.colors.primary.text[10]};
+ }
+
+ &[data-active='false'] {
+ background-color: ${theme.colors.highlight[100]};
+ }
+`;
+
+type TabButtonProps = {
+ controls: string;
+ id: string;
+ isActive: boolean;
+ onSelect: () => void;
+ tab: TabType;
+};
+
+export function TabButton({
+ controls,
+ id,
+ isActive,
+ onSelect,
+ tab,
+}: TabButtonProps) {
+ const renderText = useRenderMessage();
+
+ const iconColor = isActive
+ ? theme.colors.highlight[100]
+ : theme.colors.secondary.text[100];
+
+ const Icon = INFORMATIVE_ICONS[tab.icon as keyof typeof INFORMATIVE_ICONS];
+
+ return (
+
+
+
+ {Icon ? : null}
+
+
+ );
+}
diff --git a/packages/twenty-website-new/src/sections/Tabs/components/TabButtons.tsx b/packages/twenty-website-new/src/sections/Tabs/components/TabButtons.tsx
new file mode 100644
index 0000000000..7dea65d671
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/Tabs/components/TabButtons.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import type { TabType } from '@/sections/Tabs/types';
+import { theme } from '@/theme';
+import { styled } from '@linaria/react';
+
+import { TabButton } from './TabButton';
+
+const TabButtonsGrid = styled.div`
+ display: grid;
+ gap: ${theme.spacing(2)};
+ grid-template-columns: minmax(0, max-content);
+ justify-content: center;
+ width: 100%;
+
+ @media (min-width: ${theme.breakpoints.md}px) {
+ grid-template-columns: repeat(2, minmax(0, max-content));
+ }
+
+ @media (min-width: ${theme.breakpoints.lg}px) {
+ grid-template-columns: repeat(4, minmax(0, max-content));
+ }
+`;
+
+type TabButtonsProps = {
+ activeIndex: number;
+ idPrefix: string;
+ onSelect: (index: number) => void;
+ tabs: TabType[];
+};
+
+export function TabButtons({
+ activeIndex,
+ idPrefix,
+ onSelect,
+ tabs,
+}: TabButtonsProps) {
+ return (
+
+ {tabs.map((tab, index) => (
+ onSelect(index)}
+ tab={tab}
+ />
+ ))}
+
+ );
+}
diff --git a/packages/twenty-website-new/src/sections/Tabs/components/TabGroup.tsx b/packages/twenty-website-new/src/sections/Tabs/components/TabGroup.tsx
new file mode 100644
index 0000000000..1de2429852
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/Tabs/components/TabGroup.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import { useId, useState } from 'react';
+
+import { theme } from '@/theme';
+import { styled } from '@linaria/react';
+
+import type { TabType } from '@/sections/Tabs/types';
+
+import { TabButtons } from './TabButtons';
+import { TabVisuals } from './TabVisuals';
+
+const StyledTabGroup = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ row-gap: ${theme.spacing(18)};
+ width: 100%;
+`;
+
+type TabGroupProps = {
+ tabs: TabType[];
+};
+
+export function TabGroup({ tabs }: TabGroupProps) {
+ const [activeIndex, setActiveIndex] = useState(0);
+ const idPrefix = useId();
+
+ return (
+
+
+
+
+ );
+}
diff --git a/packages/twenty-website-new/src/sections/Tabs/components/TabVisuals.tsx b/packages/twenty-website-new/src/sections/Tabs/components/TabVisuals.tsx
new file mode 100644
index 0000000000..b18612cf3f
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/Tabs/components/TabVisuals.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { Image } from '@/design-system/components';
+import type { TabType } from '@/sections/Tabs/types';
+import { theme } from '@/theme';
+import { styled } from '@linaria/react';
+
+const VisualRoot = styled.div`
+ width: 100%;
+
+ @media (min-width: ${theme.breakpoints.md}px) {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 995px;
+ }
+`;
+
+const VisualImage = styled(Image)`
+ border-radius: ${theme.radius(1)};
+`;
+
+type TabVisualsProps = {
+ activeIndex: number;
+ panelId: string;
+ tabId: string;
+ tabs: TabType[];
+};
+
+export function TabVisuals({
+ activeIndex,
+ panelId,
+ tabId,
+ tabs,
+}: TabVisualsProps) {
+ const tab = tabs[activeIndex];
+
+ if (!tab) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/twenty-website-new/src/sections/Tabs/components/index.ts b/packages/twenty-website-new/src/sections/Tabs/components/index.ts
new file mode 100644
index 0000000000..8dabb1afc6
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/Tabs/components/index.ts
@@ -0,0 +1,7 @@
+import { Root } from './Root';
+import { TabGroup } from './TabGroup';
+
+export const Tabs = {
+ Root,
+ TabGroup,
+};
diff --git a/packages/twenty-website-new/src/sections/Tabs/types/index.ts b/packages/twenty-website-new/src/sections/Tabs/types/index.ts
new file mode 100644
index 0000000000..cff9dc7f44
--- /dev/null
+++ b/packages/twenty-website-new/src/sections/Tabs/types/index.ts
@@ -0,0 +1,9 @@
+import type { MessageDescriptor } from '@lingui/core';
+
+import type { ImageType } from '@/design-system/components/Image';
+
+export type TabType = {
+ body: MessageDescriptor;
+ icon: string;
+ image: ImageType;
+};