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; +};