First PR to bring in the new twenty website. (#19035)
This PR contains Menu, Hero, TrustedBy, Problem, ThreeCards and Footer sections of the new website. Most components in there match the Figma designs, except for two things. - Zoom levels on 3D illustrations from Endless Tools. - Menu needs to have the same color as Hero - it's not happening at the moment since Menu is in the layout, not nested inside pages or Hero. Images are placeholders (same as Figma).
@@ -203,6 +203,7 @@
|
||||
"packages/twenty-utils",
|
||||
"packages/twenty-zapier",
|
||||
"packages/twenty-website",
|
||||
"packages/twenty-website-new",
|
||||
"packages/twenty-docs",
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# DB
|
||||
*.sqlite
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules"],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": "off",
|
||||
"no-control-regex": "off",
|
||||
"no-debugger": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-redeclare": "off",
|
||||
"import/no-duplicates": "error",
|
||||
"typescript/no-redeclare": "error",
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"typescript/consistent-type-imports": ["error", {
|
||||
"prefer": "type-imports",
|
||||
"fixStyle": "inline-type-imports"
|
||||
}],
|
||||
"typescript/explicit-function-return-type": "off",
|
||||
"typescript/explicit-module-boundary-types": "off",
|
||||
"typescript/no-empty-object-type": ["error", {
|
||||
"allowInterfaces": "with-single-extends"
|
||||
}],
|
||||
"typescript/no-empty-function": "off",
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/no-unused-vars": ["warn", {
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}],
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-key": "off",
|
||||
"react/display-name": "off",
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/jsx-no-useless-fragment": "off",
|
||||
"react/jsx-props-no-spreading": ["error", { "explicitSpread": "ignore" }],
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Twenty-Website
|
||||
|
||||
This is used for the marketing website (twenty.com).
|
||||
This is not related in any way to the main app, which you can find in twenty-front and twenty-server.
|
||||
@@ -0,0 +1,5 @@
|
||||
import withLinaria, { type LinariaConfig } from 'next-with-linaria';
|
||||
|
||||
const nextConfig: LinariaConfig = { reactCompiler: true };
|
||||
|
||||
module.exports = withLinaria(nextConfig);
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "twenty-website-new",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"nx": "NX_DEFAULT_PROJECT=twenty-website-new node ../../node_modules/nx/bin/nx.js",
|
||||
"dev": "npx next dev",
|
||||
"build": "npx next build",
|
||||
"start": "npx next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@base-ui/utils": "^0.2.6",
|
||||
"@linaria/core": "^7.0.0",
|
||||
"@linaria/react": "^7.0.1",
|
||||
"@wyw-in-js/babel-preset": "^0.8.1",
|
||||
"next": "16.1.7",
|
||||
"next-with-linaria": "^1.3.0",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"babel-plugin-react-compiler": "1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "twenty-website-new",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/twenty-website-new/src",
|
||||
"projectType": "application",
|
||||
"tags": ["scope:website"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": ["production", "^production"],
|
||||
"outputs": ["{projectRoot}/.next"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npx next build"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"dev": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npx next dev"
|
||||
}
|
||||
},
|
||||
"start": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"dependsOn": ["build"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npx next start"
|
||||
}
|
||||
},
|
||||
"lint": {},
|
||||
"lint:diff-with-main": {},
|
||||
"typecheck": {},
|
||||
"fmt": {
|
||||
"options": {
|
||||
"files": "."
|
||||
},
|
||||
"configurations": {
|
||||
"fix": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 473 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 954 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,12 @@
|
||||
import { HeroDataType } from '@/sections/Hero/types';
|
||||
|
||||
export const HERO_DATA: HeroDataType = {
|
||||
heading: [
|
||||
{ text: 'Build', fontFamily: 'sans' },
|
||||
{ text: ' your Enterprise CRM ', fontFamily: 'serif' },
|
||||
{ text: 'at AI Speed', fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: 'Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves.',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ProblemDataType } from '@/sections/Problem/types';
|
||||
|
||||
export const PROBLEM_DATA: ProblemDataType = {
|
||||
eyebrow: { heading: { text: 'The Problem:', fontFamily: 'sans' } },
|
||||
heading: [
|
||||
{ text: 'A custom CRM gives your org an edge, ', fontFamily: 'serif' },
|
||||
{ text: 'but building one comes with tradeoffs', fontFamily: 'sans' },
|
||||
],
|
||||
points: [
|
||||
{
|
||||
heading: { text: 'The Giant Monolith', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Proprietary languages, slow deployment cycles, and "black box" logic.',
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: { text: 'The In-house Burden', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: "It's fragile. V1 ships quickly, but maintaining and making changes is a long term burden.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ThreeCardsFeatureCardsDataType } from '@/sections/ThreeCards/types';
|
||||
|
||||
export const THREE_CARDS_FEATURE_DATA: ThreeCardsFeatureCardsDataType = {
|
||||
eyebrow: {
|
||||
heading: {
|
||||
text: 'Skip the clunky UX that always comes with custom.',
|
||||
fontFamily: 'sans',
|
||||
},
|
||||
},
|
||||
heading: [
|
||||
{ text: 'Make your GTM team happy with ', fontFamily: 'serif' },
|
||||
{ text: "a CRM they'll love", fontFamily: 'sans' },
|
||||
],
|
||||
featureCards: [
|
||||
{
|
||||
heading: { text: 'Familiar, modern interface', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: "Twenty makes it simple. It's clean, intuitive, and built to feel like Notion.",
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: { text: 'Fast path to action', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Smart patterns, shortcuts, and layouts make everyday tasks faster and easier to execute.',
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: { text: 'Live data and AI Built', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Everything updates in real time, with AI chat always ready to help you move faster.',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ThreeCardsIllustrationDataType } from '@/sections/ThreeCards/types';
|
||||
|
||||
export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
|
||||
eyebrow: {
|
||||
heading: { text: 'Stop settling for trade-offs.', fontFamily: 'sans' },
|
||||
},
|
||||
heading: [
|
||||
{ text: 'Assemble, iterate and adapt a robust CRM,', fontFamily: 'serif' },
|
||||
{ text: " that's quick to flex", fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: 'Compose your CRM and interal apps with a single exesibility toolkit.',
|
||||
},
|
||||
illustrationCards: [
|
||||
{
|
||||
heading: { text: 'Production grade quality', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: "What stood out with Twenty wasn't just flexibility it was the quality. The system feels stable, polished, and ready for real teams with real stakes.",
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: {
|
||||
role: { text: 'Head of Engineering' },
|
||||
company: { text: 'Mid-Market Fintech' },
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: { text: 'AI for rapid iterations', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Twenty removes friction from iteration and lets us adapt our CRM as the business changes.',
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: {
|
||||
role: { text: 'Head of Engineering' },
|
||||
company: { text: 'Mid-Market Fintech' },
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: { text: 'Control without drag', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: "It's a modern, open-source alternative to Salesforce/Hubspot that lets you manage your customer relationships in a secure + privacy-first way.",
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: {
|
||||
role: { text: 'Head of Engineering' },
|
||||
company: { text: 'Mid-Market Fintech' },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { TrustedByDataType } from '@/sections/TrustedBy/types';
|
||||
|
||||
export const TRUSTED_BY_DATA: TrustedByDataType = {
|
||||
separator: { text: 'trusted by' },
|
||||
clientCountLabel: { text: '+ 10 000 other clients' },
|
||||
logos: [{ icon: 'flexport' }, { icon: 'zapier' }],
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { HERO_DATA } from '@/app/(home)/constants/hero';
|
||||
import { PROBLEM_DATA } from '@/app/(home)/constants/problem';
|
||||
import { THREE_CARDS_FEATURE_DATA } from '@/app/(home)/constants/three-cards-feature';
|
||||
import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/(home)/constants/three-cards-illustration';
|
||||
import { TRUSTED_BY_DATA } from '@/app/(home)/constants/trusted-by';
|
||||
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { Hero } from '@/sections/Hero/components';
|
||||
import { Problem } from '@/sections/Problem/components';
|
||||
import { ThreeCards } from '@/sections/ThreeCards/components';
|
||||
import { TrustedBy } from '@/sections/TrustedBy/components';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<Hero.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<Hero.Heading page={Pages.Home} segments={HERO_DATA.heading} />
|
||||
<Hero.Body page={Pages.Home} body={HERO_DATA.body} size="sm" />
|
||||
<Hero.Cta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Get started"
|
||||
type="anchor"
|
||||
variant="contained"
|
||||
/>
|
||||
</Hero.Cta>
|
||||
<Hero.HomeVisual />
|
||||
</Hero.Root>
|
||||
|
||||
<TrustedBy.Root>
|
||||
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
|
||||
<TrustedBy.Logos
|
||||
clientCountLabel={TRUSTED_BY_DATA.clientCountLabel}
|
||||
logos={TRUSTED_BY_DATA.logos}
|
||||
/>
|
||||
</TrustedBy.Root>
|
||||
|
||||
<Problem.Root>
|
||||
<Problem.Visual />
|
||||
<Problem.Content>
|
||||
<Eyebrow
|
||||
colorScheme="primary"
|
||||
heading={PROBLEM_DATA.eyebrow.heading}
|
||||
/>
|
||||
<Problem.Heading segments={PROBLEM_DATA.heading} />
|
||||
<Problem.Points points={PROBLEM_DATA.points} />
|
||||
</Problem.Content>
|
||||
</Problem.Root>
|
||||
|
||||
<ThreeCards.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<ThreeCards.Intro page={Pages.Home} align="left">
|
||||
<Eyebrow
|
||||
colorScheme="primary"
|
||||
heading={THREE_CARDS_ILLUSTRATION_DATA.eyebrow.heading}
|
||||
/>
|
||||
<Heading
|
||||
segments={THREE_CARDS_ILLUSTRATION_DATA.heading}
|
||||
size="lg"
|
||||
weight="light"
|
||||
/>
|
||||
<Body body={THREE_CARDS_ILLUSTRATION_DATA.body} size="sm" />
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.IllustrationCards
|
||||
illustrationCards={THREE_CARDS_ILLUSTRATION_DATA.illustrationCards}
|
||||
/>
|
||||
</ThreeCards.Root>
|
||||
|
||||
<ThreeCards.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<ThreeCards.Intro page={Pages.Home} align="center">
|
||||
<Eyebrow
|
||||
colorScheme="primary"
|
||||
heading={THREE_CARDS_FEATURE_DATA.eyebrow.heading}
|
||||
/>
|
||||
<Heading
|
||||
segments={THREE_CARDS_FEATURE_DATA.heading}
|
||||
size="lg"
|
||||
weight="light"
|
||||
/>
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.FeatureCards
|
||||
featureCards={THREE_CARDS_FEATURE_DATA.featureCards}
|
||||
/>
|
||||
</ThreeCards.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 170 KiB |
@@ -0,0 +1,89 @@
|
||||
import { Footer } from '@/sections/Footer/components';
|
||||
import { Menu } from '@/sections/Menu/components';
|
||||
import { theme } from '@/theme';
|
||||
import { cssVariables } from '@/theme/css-variables';
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { Metadata } from 'next';
|
||||
import { Aleo, Azeret_Mono, Host_Grotesk } from 'next/font/google';
|
||||
|
||||
const hostGrotesk = Host_Grotesk({
|
||||
subsets: ['latin'],
|
||||
weight: ['300', '400', '500', '600'],
|
||||
variable: '--font-sans',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const aleo = Aleo({
|
||||
subsets: ['latin'],
|
||||
weight: ['300'],
|
||||
variable: '--font-serif',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const azeretMono = Azeret_Mono({
|
||||
subsets: ['latin'],
|
||||
weight: ['300', '500'],
|
||||
variable: '--font-mono',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
css`
|
||||
:global(*),
|
||||
:global(*::before),
|
||||
:global(*::after) {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(html) {
|
||||
background-color: ${theme.colors.primary.background[100]};
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
display: flex;
|
||||
font-family: ${theme.font.family.sans};
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMain = styled.main`
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Twenty — Open Source CRM',
|
||||
description: 'Modular, scalable open source CRM for modern teams.',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${cssVariables} ${hostGrotesk.variable} ${aleo.variable} ${azeretMono.variable}`}
|
||||
>
|
||||
<Menu.Root>
|
||||
<Menu.Logo />
|
||||
<Menu.Nav />
|
||||
<Menu.Social />
|
||||
<Menu.Cta />
|
||||
</Menu.Root>
|
||||
<StyledMain>{children}</StyledMain>
|
||||
<Footer.Root>
|
||||
<Footer.Logo />
|
||||
<Footer.Nav />
|
||||
<Footer.Bottom>
|
||||
<Footer.Social />
|
||||
</Footer.Bottom>
|
||||
</Footer.Root>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { HeroDataType } from '@/sections/Hero/types';
|
||||
|
||||
export const HERO_DATA: HeroDataType = {
|
||||
heading: [
|
||||
{ text: 'Become a ', fontFamily: 'serif' },
|
||||
{ text: 'Twenty Partner', fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: "We're building the #1 open-source CRM, but we can't do it alone. Join our partner ecosystem and grow with us.",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ThreeCardsIllustrationDataType } from '@/sections/ThreeCards/types';
|
||||
|
||||
export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
|
||||
eyebrow: {
|
||||
heading: {
|
||||
text: 'Which partner program is right for you?',
|
||||
fontFamily: 'sans',
|
||||
},
|
||||
},
|
||||
heading: [
|
||||
{ text: 'Find the program that fits your business ', fontFamily: 'serif' },
|
||||
{ text: 'and unlock new opportunities with Twenty', fontFamily: 'sans' },
|
||||
],
|
||||
body: { text: '' },
|
||||
illustrationCards: [
|
||||
{
|
||||
heading: { text: 'Technology Partners', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Build integrations that connect Twenty with the tools your customers already use. Help us expand the Twenty ecosystem.',
|
||||
},
|
||||
benefits: [
|
||||
{ text: 'Co-marketing opportunities' },
|
||||
{ text: 'Listing on Twenty integrations page' },
|
||||
{ text: 'Soon: earn revenue' },
|
||||
],
|
||||
attribution: undefined,
|
||||
},
|
||||
{
|
||||
heading: { text: 'Content & Community Partners', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: "Share Twenty with your audience and help shape the future of open-source CRM. We're looking for creators, educators, and community builders who want to showcase great software.",
|
||||
},
|
||||
benefits: [
|
||||
{ text: 'Revenue share for referred customers' },
|
||||
{ text: 'Exclusive content collaboration opportunities' },
|
||||
{ text: 'Marketing assets & brand resources' },
|
||||
],
|
||||
attribution: undefined,
|
||||
},
|
||||
{
|
||||
heading: { text: 'Solutions Partners', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Help customers implement, customize, and succeed with Twenty. Combine sales and services to grow your business.',
|
||||
},
|
||||
benefits: [
|
||||
{ text: 'Resale discounts & revenue share' },
|
||||
{ text: 'Marketplace listing' },
|
||||
{ text: 'Dedicated partner support' },
|
||||
],
|
||||
attribution: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { TRUSTED_BY_DATA } from '@/app/(home)/constants/trusted-by';
|
||||
import { HERO_DATA } from '@/app/partner/constants/hero';
|
||||
import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/partner/constants/three-cards-illustration';
|
||||
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { Hero } from '@/sections/Hero/components';
|
||||
import { ThreeCards } from '@/sections/ThreeCards/components';
|
||||
import { TrustedBy } from '@/sections/TrustedBy/components';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export default function PartnerPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<Hero.Heading page={Pages.Partner} segments={HERO_DATA.heading} />
|
||||
<Hero.Body page={Pages.Partner} body={HERO_DATA.body} />
|
||||
<Hero.Cta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Become a partner"
|
||||
type="anchor"
|
||||
variant="outlined"
|
||||
/>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Find a partner"
|
||||
type="anchor"
|
||||
variant="contained"
|
||||
/>
|
||||
</Hero.Cta>
|
||||
<Hero.Illustration
|
||||
src="https://app.endlesstools.io/embed/1c6c8259-3276-4cf2-84d8-6b7e87e7ec95"
|
||||
title="Endless Tools Editor"
|
||||
backgroundColor={theme.colors.secondary.background[100]}
|
||||
/>
|
||||
</Hero.Root>
|
||||
|
||||
<TrustedBy.Root>
|
||||
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
|
||||
<TrustedBy.Logos
|
||||
clientCountLabel={TRUSTED_BY_DATA.clientCountLabel}
|
||||
logos={TRUSTED_BY_DATA.logos}
|
||||
/>
|
||||
</TrustedBy.Root>
|
||||
|
||||
<ThreeCards.Root backgroundColor={theme.colors.secondary.background[5]}>
|
||||
<ThreeCards.Intro align="left">
|
||||
<Eyebrow
|
||||
colorScheme="primary"
|
||||
heading={THREE_CARDS_ILLUSTRATION_DATA.eyebrow.heading}
|
||||
/>
|
||||
<Heading
|
||||
segments={THREE_CARDS_ILLUSTRATION_DATA.heading}
|
||||
size="lg"
|
||||
weight="light"
|
||||
/>
|
||||
<Body body={THREE_CARDS_ILLUSTRATION_DATA.body} size="sm" />
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.IllustrationCards
|
||||
illustrationCards={THREE_CARDS_ILLUSTRATION_DATA.illustrationCards}
|
||||
variant="simple"
|
||||
/>
|
||||
</ThreeCards.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { HeroDataType } from '@/sections/Hero/types';
|
||||
|
||||
export const HERO_DATA: HeroDataType = {
|
||||
heading: [
|
||||
{ text: 'A pricing that scales ', fontFamily: 'serif' },
|
||||
{ text: 'with your need', fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: "We're building the #1 open-source CRM, but we can't do it alone. Join our partner ecosystem and grow with us.",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { HERO_DATA } from '@/app/pricing/constants/hero';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { Hero } from '@/sections/Hero/components';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export default function PricingPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero.Root backgroundColor={theme.colors.secondary.background[5]}>
|
||||
<Hero.Heading page={Pages.Pricing} segments={HERO_DATA.heading} />
|
||||
<Hero.Body page={Pages.Pricing} body={HERO_DATA.body} />
|
||||
</Hero.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { HeroDataType } from '@/sections/Hero/types';
|
||||
|
||||
export const HERO_DATA: HeroDataType = {
|
||||
heading: [
|
||||
{ text: 'The CRM that moves ', fontFamily: 'serif' },
|
||||
{ text: 'as fast as you do', fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: 'Modern interface. AI assistance. All the features you need, ready from day one.',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ThreeCardsIllustrationDataType } from '@/sections/ThreeCards/types';
|
||||
|
||||
export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
|
||||
eyebrow: {
|
||||
heading: { text: 'Stop settling for trade-offs.', fontFamily: 'sans' },
|
||||
},
|
||||
heading: [
|
||||
{ text: 'A modern CRM with ', fontFamily: 'serif' },
|
||||
{ text: 'an intuitive interface', fontFamily: 'sans' },
|
||||
],
|
||||
body: { text: '' },
|
||||
illustrationCards: [
|
||||
{
|
||||
heading: { text: 'Built for speed', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Fly through your workspace with shortcuts and short load times.',
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: undefined,
|
||||
},
|
||||
{
|
||||
heading: { text: 'Real-time data', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'See updates as they happen. Work with your team and agents seamlessly.',
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: undefined,
|
||||
},
|
||||
{
|
||||
heading: { text: 'Stay in Flow', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'AI chat, settings, and records in a side panels for fast, single-screen access.',
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { TRUSTED_BY_DATA } from '@/app/(home)/constants/trusted-by';
|
||||
import { HERO_DATA } from '@/app/product/constants/hero';
|
||||
import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/product/constants/three-cards';
|
||||
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { Hero } from '@/sections/Hero/components';
|
||||
import { ThreeCards } from '@/sections/ThreeCards/components';
|
||||
import { TrustedBy } from '@/sections/TrustedBy/components';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export default function ProductPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<Hero.Heading page={Pages.Product} segments={HERO_DATA.heading} />
|
||||
<Hero.Body page={Pages.Product} body={HERO_DATA.body} />
|
||||
<Hero.Cta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Get started"
|
||||
type="anchor"
|
||||
variant="contained"
|
||||
/>
|
||||
</Hero.Cta>
|
||||
<Hero.Illustration
|
||||
src="https://app.endlesstools.io/embed/0bcf3ac2-58cf-4cd5-90bd-e8fada9816a9"
|
||||
title="Endless Tools Editor"
|
||||
backgroundColor={theme.colors.secondary.background[5]}
|
||||
/>
|
||||
|
||||
<TrustedBy.Root>
|
||||
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
|
||||
<TrustedBy.Logos
|
||||
clientCountLabel={TRUSTED_BY_DATA.clientCountLabel}
|
||||
logos={TRUSTED_BY_DATA.logos}
|
||||
/>
|
||||
</TrustedBy.Root>
|
||||
|
||||
<ThreeCards.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<ThreeCards.Intro page={Pages.Product} align="left">
|
||||
<Eyebrow
|
||||
colorScheme="primary"
|
||||
heading={THREE_CARDS_ILLUSTRATION_DATA.eyebrow.heading}
|
||||
/>
|
||||
<Heading
|
||||
segments={THREE_CARDS_ILLUSTRATION_DATA.heading}
|
||||
size="lg"
|
||||
weight="light"
|
||||
/>
|
||||
<Body body={THREE_CARDS_ILLUSTRATION_DATA.body} size="sm" />
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.IllustrationCards
|
||||
illustrationCards={THREE_CARDS_ILLUSTRATION_DATA.illustrationCards}
|
||||
/>
|
||||
</ThreeCards.Root>
|
||||
</Hero.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { HeroDataType } from '@/sections/Hero/types';
|
||||
|
||||
export const HERO_DATA: HeroDataType = {
|
||||
heading: [
|
||||
{ text: 'The future of CRM is built, ', fontFamily: 'serif' },
|
||||
{ text: 'not bought.', fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: 'Most packaged software makes companies more similar. CRM is no exception. AI is turning software into a commodity. That does not make your go-to-market a commodity. It makes it the battleground.',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { HERO_DATA } from '@/app/why-twenty/constants/hero';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { Hero } from '@/sections/Hero/components';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export default function WhyTwentyPage() {
|
||||
return (
|
||||
<Hero.Root backgroundColor={theme.colors.secondary.background[100]}>
|
||||
<Hero.Heading page={Pages.WhyTwenty} segments={HERO_DATA.heading} size="xl" />
|
||||
<Hero.Body page={Pages.WhyTwenty} body={HERO_DATA.body} />
|
||||
<Hero.WhyTwentyVisual />
|
||||
</Hero.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { theme } from '@/theme';
|
||||
import { css } from '@linaria/core';
|
||||
import { BodyType } from './types/Body';
|
||||
|
||||
const bodyClassName = css`
|
||||
margin: 0;
|
||||
|
||||
&[data-family='sans'] {
|
||||
font-family: ${theme.font.family.sans};
|
||||
}
|
||||
|
||||
&[data-family='serif'] {
|
||||
font-family: ${theme.font.family.serif};
|
||||
}
|
||||
|
||||
&[data-family='mono'] {
|
||||
font-family: ${theme.font.family.mono};
|
||||
}
|
||||
|
||||
&[data-weight='light'] {
|
||||
font-weight: ${theme.font.weight.light};
|
||||
}
|
||||
|
||||
&[data-weight='regular'] {
|
||||
font-weight: ${theme.font.weight.regular};
|
||||
}
|
||||
|
||||
&[data-weight='medium'] {
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
}
|
||||
|
||||
&[data-size='md'] {
|
||||
font-size: ${theme.font.size(4)};
|
||||
line-height: ${theme.lineHeight(5.5)};
|
||||
}
|
||||
|
||||
&[data-size='sm'] {
|
||||
font-size: ${theme.font.size(4)};
|
||||
line-height: ${theme.lineHeight(5.5)};
|
||||
}
|
||||
|
||||
&[data-size='xs'] {
|
||||
font-size: ${theme.font.size(3)};
|
||||
line-height: ${theme.lineHeight(3.5)};
|
||||
}
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
&[data-size='md'] {
|
||||
font-size: ${theme.font.size(4.5)};
|
||||
line-height: ${theme.lineHeight(6)};
|
||||
}
|
||||
|
||||
&[data-size='sm'] {
|
||||
font-size: ${theme.font.size(4)};
|
||||
line-height: ${theme.lineHeight(5.5)};
|
||||
}
|
||||
|
||||
&[data-size='xs'] {
|
||||
font-size: ${theme.font.size(3)};
|
||||
line-height: ${theme.lineHeight(3.5)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type BodyAs = 'p' | 'span' | 'div';
|
||||
export type BodyFamily = 'sans' | 'serif' | 'mono';
|
||||
export type BodyWeight = 'light' | 'regular' | 'medium';
|
||||
export type BodySize = 'md' | 'sm' | 'xs';
|
||||
|
||||
export type BodyProps = {
|
||||
as?: BodyAs;
|
||||
body: BodyType;
|
||||
family?: BodyFamily;
|
||||
weight?: BodyWeight;
|
||||
size?: BodySize;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Body({
|
||||
as: Tag = 'p',
|
||||
body,
|
||||
family = 'sans',
|
||||
weight = 'regular',
|
||||
size = 'md',
|
||||
className,
|
||||
}: BodyProps) {
|
||||
const rootClassName = [bodyClassName, className].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<Tag
|
||||
className={rootClassName}
|
||||
data-family={family}
|
||||
data-weight={weight}
|
||||
data-size={size}
|
||||
>
|
||||
{body.text}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type BodyType = {
|
||||
text: string;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import { ButtonShape } from './ButtonShape';
|
||||
|
||||
export const buttonBaseStyles = `
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: ${theme.radius(2)};
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-family: ${theme.font.family.mono};
|
||||
font-size: ${theme.font.size(3)};
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
height: ${theme.spacing(10)};
|
||||
justify-content: center;
|
||||
letter-spacing: 0;
|
||||
padding: 0 ${theme.spacing(5)};
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid ${theme.colors.highlight[100]};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Label = styled.span`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&[data-color='primary'] {
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
}
|
||||
|
||||
&[data-color='secondary'] {
|
||||
color: ${theme.colors.secondary.text[100]};
|
||||
}
|
||||
`;
|
||||
|
||||
export type BaseButtonProps = {
|
||||
color: 'primary' | 'secondary';
|
||||
label: string;
|
||||
variant: 'contained' | 'outlined';
|
||||
};
|
||||
|
||||
export function BaseButton({ color, label, variant }: BaseButtonProps) {
|
||||
let fillColor: string;
|
||||
let strokeColor: string;
|
||||
let labelColor: 'primary' | 'secondary';
|
||||
|
||||
switch (`${variant}.${color}`) {
|
||||
case 'contained.primary':
|
||||
fillColor = theme.colors.primary.background[100];
|
||||
strokeColor = 'none';
|
||||
labelColor = 'primary';
|
||||
break;
|
||||
case 'contained.secondary':
|
||||
fillColor = theme.colors.secondary.background[100];
|
||||
strokeColor = 'none';
|
||||
labelColor = 'secondary';
|
||||
break;
|
||||
case 'outlined.primary':
|
||||
fillColor = 'none';
|
||||
strokeColor = theme.colors.primary.background[100];
|
||||
labelColor = 'secondary';
|
||||
break;
|
||||
case 'outlined.secondary':
|
||||
fillColor = 'none';
|
||||
strokeColor = theme.colors.secondary.background[100];
|
||||
labelColor = 'primary';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled button appearance: ${variant} ${color}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonShape fillColor={fillColor} strokeColor={strokeColor} />
|
||||
<Label data-color={labelColor}>{label}</Label>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { styled } from "@linaria/react";
|
||||
|
||||
interface ButtonShapeProps {
|
||||
fillColor: string;
|
||||
strokeColor: string;
|
||||
}
|
||||
|
||||
const LEFT_FILL = "M4 0 A4 4 0 0 0 0 4 V36 A4 4 0 0 0 4 40 Z";
|
||||
const LEFT_OUTLINE = "M4 0.5 A3.5 3.5 0 0 0 0.5 4 V36 A3.5 3.5 0 0 0 4 39.5";
|
||||
|
||||
const RIGHT_FILL = "M0 0 h11 a4 4 0 0 1 4 4 v20.523 a6 6 0 0 1 -1.544 4.019 l-8.548 9.477 A6 6 0 0 1 0.453 40 H0 Z";
|
||||
const RIGHT_OUTLINE = "M0 0.5 h11 a3.5 3.5 0 0 1 3.5 3.5 v20.523 a5.5 5.5 0 0 1 -1.416 3.684 l-8.547 9.477 a5.5 5.5 0 0 1 -4.084 1.816 H0";
|
||||
|
||||
const ShapeContainer = styled.div`
|
||||
display: flex;
|
||||
height: 40px;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
`;
|
||||
|
||||
const LeftCap = styled.svg`
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const MiddleSegment = styled.svg`
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
margin: 0 -1px;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const RightCap = styled.svg`
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export function ButtonShape({ fillColor, strokeColor }: ButtonShapeProps) {
|
||||
const isOutline = fillColor === "none";
|
||||
|
||||
return (
|
||||
<ShapeContainer>
|
||||
<LeftCap width="4" height="40" viewBox="0 0 4 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{isOutline ? (
|
||||
<path d={LEFT_OUTLINE} fill={fillColor} stroke={strokeColor} strokeWidth="1" />
|
||||
) : (
|
||||
<path d={LEFT_FILL} fill={fillColor} stroke={strokeColor} />
|
||||
)}
|
||||
</LeftCap>
|
||||
|
||||
<MiddleSegment width="100%" height="40" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{isOutline ? (
|
||||
<>
|
||||
<line x1="0" y1="0.5" x2="100%" y2="0.5" stroke={strokeColor} strokeWidth="1" />
|
||||
<line x1="0" y1="39.5" x2="100%" y2="39.5" stroke={strokeColor} strokeWidth="1" />
|
||||
</>
|
||||
) : (
|
||||
<rect width="100%" height="40" fill={fillColor} stroke={strokeColor} />
|
||||
)}
|
||||
</MiddleSegment>
|
||||
|
||||
<RightCap width="15" height="40" viewBox="0 0 15 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{isOutline ? (
|
||||
<path d={RIGHT_OUTLINE} fill={fillColor} stroke={strokeColor} strokeWidth="1" strokeLinejoin="round" strokeLinecap="round" />
|
||||
) : (
|
||||
<path d={RIGHT_FILL} fill={fillColor} stroke={strokeColor} />
|
||||
)}
|
||||
</RightCap>
|
||||
</ShapeContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { LinkButtonType } from '@/design-system/components/Button/types/LinkButtonType';
|
||||
import { styled } from '@linaria/react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
BaseButton,
|
||||
type BaseButtonProps,
|
||||
buttonBaseStyles,
|
||||
} from './BaseButton';
|
||||
|
||||
const StyledButtonAnchor = styled.a`
|
||||
${buttonBaseStyles}
|
||||
`;
|
||||
|
||||
const StyledButtonLink = styled(Link)`
|
||||
${buttonBaseStyles}
|
||||
`;
|
||||
|
||||
export type LinkButtonPresentation = 'anchor' | 'link';
|
||||
|
||||
export type LinkButtonProps = Omit<BaseButtonProps, 'label'> &
|
||||
LinkButtonType & { type: LinkButtonPresentation };
|
||||
|
||||
export function LinkButton({
|
||||
color,
|
||||
href,
|
||||
label,
|
||||
type,
|
||||
variant,
|
||||
}: LinkButtonProps) {
|
||||
const inner = <BaseButton color={color} label={label} variant={variant} />;
|
||||
|
||||
if (type === 'anchor') {
|
||||
return (
|
||||
<StyledButtonAnchor
|
||||
href={href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{inner}
|
||||
</StyledButtonAnchor>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledButtonLink href={href}>
|
||||
{inner}
|
||||
</StyledButtonLink>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import {
|
||||
BaseButton,
|
||||
type BaseButtonProps,
|
||||
buttonBaseStyles,
|
||||
} from './BaseButton';
|
||||
import type { SubmitButtonType } from './types/SubmitButtonType';
|
||||
|
||||
const StyledSubmitButton = styled.button`
|
||||
${buttonBaseStyles}
|
||||
`;
|
||||
|
||||
export type SubmitButtonProps = Omit<BaseButtonProps, 'label'> &
|
||||
SubmitButtonType & { onClick?: () => void };
|
||||
|
||||
export function SubmitButton({
|
||||
color,
|
||||
label,
|
||||
onClick,
|
||||
variant,
|
||||
}: SubmitButtonProps) {
|
||||
return (
|
||||
<StyledSubmitButton type="submit" onClick={onClick}>
|
||||
<BaseButton color={color} label={label} variant={variant} />
|
||||
</StyledSubmitButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type LinkButtonType = { href: string; label: string };
|
||||
@@ -0,0 +1 @@
|
||||
export type SubmitButtonType = { label: string };
|
||||
@@ -0,0 +1,15 @@
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const ContainerRoot = styled.div`
|
||||
margin: 0 auto;
|
||||
max-width: ${theme.breakpoints.maxContent}px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type ContainerProps = { children: ReactNode; className?: string };
|
||||
|
||||
export function Container({ children, className }: ContainerProps) {
|
||||
return <ContainerRoot className={className}>{children}</ContainerRoot>;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Heading } from '@/design-system/components/Heading/Heading';
|
||||
import { HeadingType } from '@/design-system/components/Heading/types/Heading';
|
||||
import { RectangleFillIcon } from '@/icons';
|
||||
import { theme } from '@/theme';
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const EyebrowRow = styled.div`
|
||||
align-items: start;
|
||||
display: grid;
|
||||
gap: ${theme.spacing(2)};
|
||||
grid-template-columns: auto 1fr;
|
||||
text-align: start;
|
||||
`;
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: ${theme.lineHeight(6)};
|
||||
`;
|
||||
|
||||
const eyebrowColorPrimary = css`
|
||||
color: ${theme.colors.primary.text[60]};
|
||||
`;
|
||||
|
||||
const eyebrowColorSecondary = css`
|
||||
color: ${theme.colors.secondary.text[60]};
|
||||
`;
|
||||
|
||||
type EyebrowProps = {
|
||||
heading: HeadingType;
|
||||
colorScheme: 'primary' | 'secondary';
|
||||
};
|
||||
|
||||
export function Eyebrow({ heading, colorScheme }: EyebrowProps) {
|
||||
const colorClassName =
|
||||
colorScheme === 'primary' ? eyebrowColorPrimary : eyebrowColorSecondary;
|
||||
|
||||
return (
|
||||
<EyebrowRow>
|
||||
<IconWrapper>
|
||||
<RectangleFillIcon size={14} fillColor={theme.colors.highlight[100]} />
|
||||
</IconWrapper>
|
||||
<Heading
|
||||
as="h3"
|
||||
className={colorClassName}
|
||||
segments={{ fontFamily: heading.fontFamily, text: heading.text }}
|
||||
size="xs"
|
||||
weight="medium"
|
||||
/>
|
||||
</EyebrowRow>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { HeadingType } from '@/design-system/components/Heading/types/Heading';
|
||||
|
||||
export type EyebrowType = { heading: HeadingType };
|
||||
@@ -0,0 +1,130 @@
|
||||
import { theme } from '@/theme';
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
import { HeadingType } from './types/Heading';
|
||||
|
||||
const headingRootClassName = css`
|
||||
margin: 0;
|
||||
|
||||
&[data-weight='light'] {
|
||||
font-weight: ${theme.font.weight.light};
|
||||
}
|
||||
|
||||
&[data-weight='regular'] {
|
||||
font-weight: ${theme.font.weight.regular};
|
||||
}
|
||||
|
||||
&[data-weight='medium'] {
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
}
|
||||
|
||||
&[data-size='xl'] {
|
||||
font-size: ${theme.font.size(15)};
|
||||
line-height: ${theme.lineHeight(16.5)};
|
||||
}
|
||||
|
||||
&[data-size='lg'] {
|
||||
font-size: ${theme.font.size(10)};
|
||||
line-height: ${theme.lineHeight(11.5)};
|
||||
}
|
||||
|
||||
&[data-size='md'] {
|
||||
font-size: ${theme.font.size(10)};
|
||||
line-height: ${theme.lineHeight(11.5)};
|
||||
}
|
||||
|
||||
&[data-size='sm'] {
|
||||
font-size: ${theme.font.size(8)};
|
||||
line-height: ${theme.lineHeight(10)};
|
||||
}
|
||||
|
||||
&[data-size='xs'] {
|
||||
font-size: ${theme.font.size(4.5)};
|
||||
line-height: ${theme.lineHeight(6)};
|
||||
}
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
&[data-size='xl'] {
|
||||
font-size: ${theme.font.size(20)};
|
||||
line-height: ${theme.lineHeight(21.5)};
|
||||
}
|
||||
|
||||
&[data-size='lg'] {
|
||||
font-size: ${theme.font.size(15)};
|
||||
line-height: ${theme.lineHeight(16.5)};
|
||||
}
|
||||
|
||||
&[data-size='md'] {
|
||||
font-size: ${theme.font.size(12)};
|
||||
line-height: ${theme.lineHeight(14)};
|
||||
}
|
||||
|
||||
&[data-size='sm'] {
|
||||
font-size: ${theme.font.size(8)};
|
||||
line-height: ${theme.lineHeight(10)};
|
||||
}
|
||||
|
||||
&[data-size='xs'] {
|
||||
font-size: ${theme.font.size(5.5)};
|
||||
line-height: ${theme.lineHeight(7)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSpan = styled.span`
|
||||
&[data-family='sans'] {
|
||||
font-family: ${theme.font.family.sans};
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
&[data-family='serif'] {
|
||||
font-family: ${theme.font.family.serif};
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
&[data-family='mono'] {
|
||||
font-family: ${theme.font.family.mono};
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
`;
|
||||
|
||||
export type HeadingAs = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
export type HeadingFamily = 'sans' | 'serif' | 'mono';
|
||||
export type HeadingWeight = 'light' | 'regular' | 'medium';
|
||||
export type HeadingSize = 'xl' | 'lg' | 'md' | 'sm' | 'xs';
|
||||
|
||||
export type HeadingProps = {
|
||||
as?: HeadingAs;
|
||||
segments: HeadingType | HeadingType[];
|
||||
weight?: HeadingWeight;
|
||||
size?: HeadingSize;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Heading({
|
||||
as: Tag = 'h1',
|
||||
segments,
|
||||
weight = 'regular',
|
||||
size = 'md',
|
||||
className,
|
||||
}: HeadingProps) {
|
||||
const rootClassName = [headingRootClassName, className]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<Tag className={rootClassName} data-weight={weight} data-size={size}>
|
||||
{Array.isArray(segments) ? (
|
||||
segments.map((segment, index) => (
|
||||
<StyledSpan key={index} data-family={segment.fontFamily}>
|
||||
{segment.text}
|
||||
</StyledSpan>
|
||||
))
|
||||
) : (
|
||||
<StyledSpan data-family={segments.fontFamily}>
|
||||
{segments.text}
|
||||
</StyledSpan>
|
||||
)}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type HeadingType = {
|
||||
fontFamily: 'sans' | 'serif' | 'mono';
|
||||
text: string;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ComponentType } from "react";
|
||||
import { theme } from "@/theme";
|
||||
import { styled } from "@linaria/react";
|
||||
|
||||
type IconComponent =
|
||||
| ComponentType<{ size: number; fillColor: string }>
|
||||
| ComponentType<{ size: number; strokeColor: string }>;
|
||||
|
||||
interface IconButtonProps {
|
||||
icon: IconComponent;
|
||||
ariaLabel: string;
|
||||
borderColor: string;
|
||||
iconFillColor: string;
|
||||
iconSize: number;
|
||||
iconStrokeColor: string;
|
||||
size: number;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
ariaExpanded?: boolean;
|
||||
}
|
||||
|
||||
const StyledButton = styled.button<{ size: number; borderColor: string }>`
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 1px solid ${(p) => p.borderColor};
|
||||
border-radius: ${theme.radius(2)};
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
height: ${(p) => p.size}px;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: ${(p) => p.size}px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid ${theme.colors.highlight[100]};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function IconButton({
|
||||
icon: Icon,
|
||||
ariaLabel,
|
||||
borderColor,
|
||||
iconFillColor,
|
||||
iconSize,
|
||||
iconStrokeColor,
|
||||
size,
|
||||
onClick,
|
||||
ariaExpanded,
|
||||
}: IconButtonProps) {
|
||||
return (
|
||||
<StyledButton
|
||||
type="button"
|
||||
size={size}
|
||||
borderColor={borderColor}
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={ariaExpanded}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon
|
||||
size={iconSize}
|
||||
strokeColor={iconStrokeColor}
|
||||
fillColor={iconFillColor}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type IllustrationType = {
|
||||
src: string;
|
||||
title: string;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import NextImage from 'next/image';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { ImageType } from './types/Image';
|
||||
|
||||
const ImageRoot = styled.div`
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type ImageComponentProps = ImageType & { className?: string };
|
||||
|
||||
export function Image({ alt, className, src }: ImageComponentProps) {
|
||||
return (
|
||||
<ImageRoot className={className}>
|
||||
<NextImage
|
||||
alt={alt}
|
||||
fill
|
||||
sizes="100vw"
|
||||
src={src}
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
</ImageRoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type ImageType = {
|
||||
alt: string;
|
||||
src: string;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export { Body } from './Body/Body';
|
||||
export { LinkButton } from './Button/LinkButton';
|
||||
export { SubmitButton } from './Button/SubmitButton';
|
||||
export { Container } from './Container/Container';
|
||||
export { Eyebrow } from './Eyebrow/Eyebrow';
|
||||
export { Heading } from './Heading/Heading';
|
||||
export { IconButton } from './IconButton/IconButton';
|
||||
export { Image } from './Image/Image';
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum Pages {
|
||||
Home = "home",
|
||||
Partner = "partner",
|
||||
Pricing = "pricing",
|
||||
Product = "product",
|
||||
WhyTwenty = "whyTwenty",
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
const FLEXPORT_VIEWBOX_WIDTH = 115;
|
||||
const FLEXPORT_VIEWBOX_HEIGHT = 27;
|
||||
|
||||
const FLEXPORT_PATH_1 =
|
||||
"M13.462 16.78V.047h5.457v16.034c0 1.13.24 1.394 1.394 1.394h.457l.649 4.423a39 39 0 0 1-3.125.169c-2.981.024-4.832-1.034-4.832-5.289M55.241 6.562l4.23-.336.41 1.274c.84-.793 2.331-1.587 5.528-1.587 5.505 0 6.78 3.87 6.78 8.198 0 4.35-1.395 8.173-7.116 8.173-2.524 0-3.726-.577-4.4-1.202v5.505h-5.456zm11.418 7.765c0-1.851-.336-3.462-2.692-3.462-2.524 0-3.27 1.299-3.27 3.462s.554 3.365 3.27 3.365c2.308-.024 2.692-1.538 2.692-3.365m24.977-7.764 4.23-.337.553 1.659c.698-.938 2.597-1.731 4.856-1.827v5.24c-3.63.024-4.183 1.106-4.183 3.27v7.452h-5.456zm11.154-.505h2.812l.89-3.197 4.206-.914v4.11h3.438v5.241h-3.438v4.568c0 1.081.169 1.634 1.875 1.634h1.563v4.543c-.817.049-1.731.073-2.765.073-4.591 0-6.105-1.467-6.105-5.433v-5.36h-2.476zM0 6.25h2.98v-.312C2.98 1.97 4.472 0 9.592 0c1.347 0 1.587.024 2.26.072v4.423h-1.514c-1.563 0-1.9.385-1.9 1.394v.361h3.414v4.616H8.414V22.02H2.98V10.866H0zm33.294 11.058c-.914.529-1.37.649-3.173.649-2.14 0-2.957-.77-3.222-2.043H37.38c.192-5.553-.264-10.048-7.26-10.048s-8.485 3.87-8.485 8.149c0 4.399 1.466 8.293 8.485 8.293 2.765 0 4.496-.505 6.347-1.707zm-3.318-6.947c2.236 0 2.404 1.057 2.5 2.356h-5.6c.192-1.515.913-2.356 3.1-2.356";
|
||||
const FLEXPORT_PATH_2 =
|
||||
"m48.198 22.043-2.669-3.822-2.668 3.822h-6.106l5.721-8.149-5.192-7.332h6.154l2.115 2.981 2.116-2.98h6.226l-5.289 7.307 5.722 8.173zm25.12-7.956c0-5.24 2.404-8.197 8.558-8.197 6.178 0 8.534 2.98 8.534 8.197 0 5.24-2.38 8.22-8.534 8.22-6.154.025-8.558-2.98-8.558-8.22m11.61.168c0-1.971-.504-3.414-3.052-3.414-2.572 0-3.053 1.443-3.053 3.414 0 1.947.505 3.414 3.053 3.414 2.572 0 3.053-1.443 3.053-3.414";
|
||||
|
||||
interface FlexportIconProps {
|
||||
size: number;
|
||||
fillColor: string;
|
||||
}
|
||||
|
||||
export function FlexportIcon({ size, fillColor }: FlexportIconProps) {
|
||||
const height = size * (FLEXPORT_VIEWBOX_HEIGHT / FLEXPORT_VIEWBOX_WIDTH);
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={height}
|
||||
viewBox={`0 0 ${FLEXPORT_VIEWBOX_WIDTH} ${FLEXPORT_VIEWBOX_HEIGHT}`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d={FLEXPORT_PATH_1} fill={fillColor} />
|
||||
<path d={FLEXPORT_PATH_2} fill={fillColor} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
const ZAPIER_VIEWBOX_WIDTH = 112;
|
||||
const ZAPIER_VIEWBOX_HEIGHT = 31;
|
||||
|
||||
const ZAPIER_PATH =
|
||||
"m26.105 20.644-.016-.05 9.885-9.05v-4.36H20.115v4.36h9.473l.015.043-9.788 9.057v4.36h16.318v-4.36zM45.87 6.77q-2.744 0-4.473.827a5.87 5.87 0 0 0-2.595 2.274 8.3 8.3 0 0 0-1.089 3.29l4.812.676q.261-1.538 1.044-2.18a3.2 3.2 0 0 1 2.068-.64q1.466 0 2.126.784c.436.522.658 1.245.658 2.198v.475h-4.627q-3.532 0-5.076 1.567t-1.54 4.136q0 2.631 1.54 3.916t3.875 1.277q2.895 0 4.454-1.992a7.4 7.4 0 0 0 1.288-2.744h.174l.664 4.36h4.36V13.952q0-3.423-1.804-5.303t-5.86-1.88m1.525 13.61c-.692.577-1.598.865-2.726.865q-1.395 0-1.938-.489a1.6 1.6 0 0 1-.545-1.24 1.63 1.63 0 0 1 .451-1.203 1.68 1.68 0 0 1 1.24-.452h4.552v.3a2.78 2.78 0 0 1-1.034 2.22M79.899 7.184h-5.151v17.823h5.15zm30.415 0q-2.183 0-3.441 1.541-.903 1.108-1.373 3.271h-.152l-.674-4.812h-4.399v17.82h5.152v-9.138q0-1.728.809-2.65t2.987-.922h1.955v-5.11zm-15.152.583q-1.804-1.074-4.55-1.07-4.324 0-6.768 2.461-2.445 2.463-2.444 6.789a10.8 10.8 0 0 0 1.11 5.037 8 8 0 0 0 3.16 3.329q2.046 1.182 4.869 1.185 2.44 0 4.078-.752a6.56 6.56 0 0 0 2.595-2.05 8.9 8.9 0 0 0 1.41-2.914l-4.438-1.242a4.1 4.1 0 0 1-1.045 1.806q-.752.753-2.52.753-2.1 0-3.133-1.204c-.499-.58-.812-1.408-.95-2.48H98.69q.076-.753.076-1.262v-1.11a9.9 9.9 0 0 0-.9-4.323 6.77 6.77 0 0 0-2.704-2.953m-4.7 3.177q3.135 0 3.554 3.084h-7.389a4.24 4.24 0 0 1 .828-1.883q1.016-1.203 3.008-1.2M77.38.002a2.9 2.9 0 0 0-2.108.81 2.8 2.8 0 0 0-.828 2.088 2.83 2.83 0 0 0 2.935 2.932 2.8 2.8 0 0 0 2.089-.828 2.87 2.87 0 0 0 .807-2.104A2.77 2.77 0 0 0 77.382.004zM66.737 6.696q-2.633 0-4.23 1.653-1.183 1.225-1.736 3.655h-.165l-.673-4.812h-4.4v22.935h5.151v-9.446h.189c.157.63.377 1.242.657 1.827a5.2 5.2 0 0 0 2.05 2.293 6 6 0 0 0 3.044.734q3.236 0 4.927-2.52 1.692-2.52 1.692-7.032 0-4.357-1.636-6.822-1.638-2.465-4.87-2.465m.522 12.914q-.9 1.306-2.82 1.306a3.32 3.32 0 0 1-2.762-1.261q-1-1.256-.998-3.44v-.338q0-2.217.998-3.395t2.762-1.164q1.953 0 2.838 1.24t.885 3.46q.006 2.295-.898 3.592zm-49.436 1.034H0v4.366h17.823z";
|
||||
|
||||
interface ZapierIconProps {
|
||||
size: number;
|
||||
fillColor: string;
|
||||
}
|
||||
|
||||
export function ZapierIcon({ size, fillColor }: ZapierIconProps) {
|
||||
const height = size * (ZAPIER_VIEWBOX_HEIGHT / ZAPIER_VIEWBOX_WIDTH);
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={height}
|
||||
viewBox={`0 0 ${ZAPIER_VIEWBOX_WIDTH} ${ZAPIER_VIEWBOX_HEIGHT}`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d={ZAPIER_PATH} fill={fillColor} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ComponentType } from "react";
|
||||
import { FlexportIcon } from "./Flexport";
|
||||
import { ZapierIcon } from "./Zapier";
|
||||
|
||||
export { FlexportIcon } from "./Flexport";
|
||||
export { ZapierIcon } from "./Zapier";
|
||||
|
||||
export type ClientIconProps = { size: number; fillColor: string };
|
||||
|
||||
export const CLIENT_ICONS: Record<string, ComponentType<ClientIconProps>> = {
|
||||
flexport: FlexportIcon,
|
||||
zapier: ZapierIcon,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
const ARROW_LEFT_PATH =
|
||||
"M.5 5.75H14m-13.5 0L5.75 11M.5 5.75 5.75.5";
|
||||
|
||||
interface ArrowLeftIconProps {
|
||||
size: number;
|
||||
strokeColor: string;
|
||||
fillColor?: string;
|
||||
}
|
||||
|
||||
export function ArrowLeftIcon({ size, strokeColor }: ArrowLeftIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size * (12 / 15)}
|
||||
viewBox="0 0 15 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d={ARROW_LEFT_PATH}
|
||||
stroke={strokeColor}
|
||||
strokeMiterlimit={10}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
const ARROW_RIGHT_PATH =
|
||||
"M14 5.75H.5m13.5 0L8.75.5M14 5.75 8.75 11";
|
||||
|
||||
interface ArrowRightIconProps {
|
||||
size: number;
|
||||
strokeColor: string;
|
||||
fillColor?: string;
|
||||
}
|
||||
|
||||
export function ArrowRightIcon({ size, strokeColor }: ArrowRightIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size * (12 / 15)}
|
||||
viewBox="0 0 15 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d={ARROW_RIGHT_PATH}
|
||||
stroke={strokeColor}
|
||||
strokeMiterlimit={10}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
const ARROW_RIGHT_UP_PATH =
|
||||
"M6.069.5.5 6.068M6.069.5H1.738m4.33 0v4.331";
|
||||
|
||||
interface ArrowRightUpIconProps {
|
||||
size: number;
|
||||
strokeColor: string;
|
||||
}
|
||||
|
||||
export function ArrowRightUpIcon({
|
||||
size,
|
||||
strokeColor,
|
||||
}: ArrowRightUpIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 6.569 6.568"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d={ARROW_RIGHT_UP_PATH}
|
||||
stroke={strokeColor}
|
||||
strokeMiterlimit={10}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
const CLOSE_PATH = "m.5.5 9.546 9.546m-9.546 0L10.046.5";
|
||||
|
||||
interface CloseIconProps {
|
||||
size: number;
|
||||
strokeColor: string;
|
||||
}
|
||||
|
||||
export function CloseIcon({ size, strokeColor }: CloseIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 10.546 10.546"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d={CLOSE_PATH}
|
||||
stroke={strokeColor}
|
||||
strokeMiterlimit={10}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
const MENU_PATH = "M.5.5H14M.5 5.75H14M.5 11H14";
|
||||
|
||||
interface MenuIconProps {
|
||||
size: number;
|
||||
strokeColor: string;
|
||||
}
|
||||
|
||||
export function MenuIcon({ size, strokeColor }: MenuIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 14.5 11.5"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d={MENU_PATH}
|
||||
stroke={strokeColor}
|
||||
strokeMiterlimit={10}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
const MINUS_PATH = "M.5 7.25H14.5";
|
||||
|
||||
interface MinusIconProps {
|
||||
size: number;
|
||||
strokeColor: string;
|
||||
}
|
||||
|
||||
export function MinusIcon({ size, strokeColor }: MinusIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d={MINUS_PATH}
|
||||
stroke={strokeColor}
|
||||
strokeMiterlimit={10}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
const PLUS_PATH = "M.5 7.25H14M7.25 14V.5";
|
||||
|
||||
interface PlusIconProps {
|
||||
size: number;
|
||||
strokeColor: string;
|
||||
}
|
||||
|
||||
export function PlusIcon({ size, strokeColor }: PlusIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d={PLUS_PATH}
|
||||
stroke={strokeColor}
|
||||
strokeMiterlimit={10}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
interface RectangleFillIconProps {
|
||||
size: number;
|
||||
fillColor: string;
|
||||
}
|
||||
|
||||
export function RectangleFillIcon({ size, fillColor }: RectangleFillIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size * (7 / 14)}
|
||||
viewBox="0 0 14 7"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width={14} height={7} rx={1} fill={fillColor} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
interface RectangleOutlineIconProps {
|
||||
size: number;
|
||||
strokeColor: string;
|
||||
}
|
||||
|
||||
export function RectangleOutlineIcon({
|
||||
size,
|
||||
strokeColor,
|
||||
}: RectangleOutlineIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size * (7 / 14)}
|
||||
viewBox="0 0 14 7"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x={0.5}
|
||||
y={0.5}
|
||||
width={13}
|
||||
height={6}
|
||||
rx={0.5}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export { CLIENT_ICONS, FlexportIcon, ZapierIcon } from "./clients";
|
||||
export { ArrowLeftIcon } from "./functional/ArrowLeft";
|
||||
export { ArrowRightIcon } from "./functional/ArrowRight";
|
||||
export { ArrowRightUpIcon } from "./functional/ArrowRightUp";
|
||||
export { CloseIcon } from "./functional/Close";
|
||||
export { MenuIcon } from "./functional/Menu";
|
||||
export { MinusIcon } from "./functional/Minus";
|
||||
export { PlusIcon } from "./functional/Plus";
|
||||
export { RectangleFillIcon } from "./functional/RectangleFill";
|
||||
export { RectangleOutlineIcon } from "./functional/RectangleOutline";
|
||||
export { Logo } from "./logo/Logo";
|
||||
export { DiscordIcon } from "./social/Discord";
|
||||
export { GitHubIcon } from "./social/GitHub";
|
||||
export { LinkedInIcon } from "./social/LinkedIn";
|
||||
export { XIcon } from "./social/X";
|
||||
@@ -0,0 +1,29 @@
|
||||
const USERS_PATH =
|
||||
"M5.499 9a7.52 7.52 0 0 1 4.492 1.502c.939.694 1.176 1.744.895 2.646h2.523c.28-.014.42-.06.48-.091a1 1 0 0 0-.11-.354 2.8 2.8 0 0 0-.709-.885 2 2 0 0 0-.396-.261.5.5 0 0 0-.175-.057v-2c.72 0 1.382.392 1.85.78.495.412.948.965 1.232 1.554.266.552.487 1.363.113 2.123-.405.824-1.277 1.145-2.2 1.19l-.024.001H8.427v-.153q-.06.004-.121.005H2.692a2.7 2.7 0 0 1-2.49-1.605c-.41-.952-.22-2.135.805-2.893A7.52 7.52 0 0 1 5.499 9m0 2c-1.213 0-2.347.404-3.303 1.11-.136.101-.175.19-.188.245a.4.4 0 0 0 .03.247c.09.205.32.398.654.398h5.614a.7.7 0 0 0 .654-.398.4.4 0 0 0 .03-.248c-.013-.054-.052-.143-.188-.244-.956-.706-2.09-1.11-3.303-1.11m.373-11a3.873 3.873 0 1 1 0 7.747 3.873 3.873 0 0 1 0-7.747m5.387.742a3.132 3.132 0 1 1-.648 6.195V4.799a1.133 1.133 0 0 0 1.78-.926 1.132 1.132 0 0 0-1.78-.928V.81q.315-.067.648-.068M5.872 2a1.873 1.873 0 1 0 0 3.746 1.873 1.873 0 0 0 0-3.746";
|
||||
|
||||
interface UsersIconProps {
|
||||
size: number;
|
||||
fillColor: string;
|
||||
}
|
||||
|
||||
export function UsersIcon({
|
||||
size,
|
||||
fillColor,
|
||||
}: UsersIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d={USERS_PATH}
|
||||
fill={fillColor}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
const LOGO_PATH =
|
||||
"M6.822 14.174c0-2.435 1.996-4.411 4.456-4.411h8.574c.125 0 .241.075.293.19a.31.31 0 0 1-.056.344l-1.88 2.023c-.326.35-.787.552-1.27.552H11.3c-.738 0-1.338.594-1.338 1.325v3.336a.78.78 0 0 1-.783.777H7.61a.78.78 0 0 1-.783-.777v-3.36zM33.5 25.553c0 2.434-1.996 4.411-4.456 4.411h-3.642c-2.46 0-4.454-1.977-4.454-4.411v-6.315c0-.43.16-.842.456-1.16l2.124-2.285a.33.33 0 0 1 .355-.081.32.32 0 0 1 .205.295v9.527c0 .73.598 1.322 1.337 1.322h3.6a1.33 1.33 0 0 0 1.337-1.322V14.197c0-.73-.599-1.325-1.337-1.325H24.84c-.481 0-.938.201-1.265.547L11.088 26.856h7.503a.78.78 0 0 1 .784.778v1.552a.78.78 0 0 1-.784.778H8.481a1.655 1.655 0 0 1-1.662-1.644v-.824c0-.412.156-.809.44-1.114l13.999-15.06a4.9 4.9 0 0 1 3.594-1.56h4.189c2.46 0 4.454 1.977 4.454 4.412v11.379z";
|
||||
|
||||
interface LogoProps {
|
||||
size: number;
|
||||
fillColor: string;
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
export function Logo({
|
||||
size,
|
||||
fillColor,
|
||||
backgroundColor,
|
||||
}: LogoProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width={40} height={40} rx={4} fill={backgroundColor} />
|
||||
<path d={LOGO_PATH} fill={fillColor} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
const DISCORD_PATH =
|
||||
"M10.518.785A10.3 10.3 0 0 0 7.99.001a.04.04 0 0 0-.04.019c-.11.194-.23.448-.315.647a9.5 9.5 0 0 0-2.84 0 7 7 0 0 0-.32-.647.04.04 0 0 0-.041-.02c-.888.153-1.737.421-2.53.785A.04.04 0 0 0 1.888.8C.277 3.206-.165 5.553.05 7.871a.04.04 0 0 0 .017.03A10.3 10.3 0 0 0 3.17 9.469a.04.04 0 0 0 .044-.014q.36-.49.634-1.033a.04.04 0 0 0-.021-.055 7 7 0 0 1-.97-.462.04.04 0 0 1-.003-.066q.098-.073.192-.15a.04.04 0 0 1 .04-.006c2.034.928 4.235.928 6.245 0a.04.04 0 0 1 .04.005q.095.078.193.151a.04.04 0 0 1-.003.066q-.463.27-.97.462a.04.04 0 0 0-.02.055q.28.543.633 1.032a.04.04 0 0 0 .044.015A10.3 10.3 0 0 0 12.355 7.9a.04.04 0 0 0 .016-.03c.26-2.68-.433-5.007-1.837-7.071a.03.03 0 0 0-.016-.015M4.152 6.46c-.612 0-1.117-.562-1.117-1.252s.495-1.253 1.117-1.253c.627 0 1.126.567 1.117 1.253 0 .69-.495 1.252-1.117 1.252m4.129 0c-.613 0-1.117-.562-1.117-1.252s.495-1.253 1.117-1.253c.627 0 1.126.567 1.116 1.253 0 .69-.49 1.252-1.116 1.252";
|
||||
|
||||
interface DiscordIconProps {
|
||||
size: number;
|
||||
fillColor: string;
|
||||
}
|
||||
|
||||
export function DiscordIcon({ size, fillColor }: DiscordIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 12.4 9.5"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d={DISCORD_PATH} fill={fillColor} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
const GITHUB_PATH =
|
||||
"M6.979 0C3.12 0 0 3.143 0 7.031a7.025 7.025 0 0 0 4.772 6.67c.347.07.474-.151.474-.337 0-.163-.012-.722-.012-1.304-1.941.419-2.345-.838-2.345-.838-.312-.815-.775-1.025-.775-1.025-.635-.43.047-.43.047-.43.705.046 1.074.721 1.074.721.624 1.071 1.63.769 2.034.582.058-.454.243-.768.439-.943-1.548-.163-3.177-.768-3.177-3.469 0-.768.277-1.396.716-1.885-.069-.175-.312-.897.069-1.863 0 0 .59-.186 1.918.722a6.7 6.7 0 0 1 1.745-.233c.589 0 1.19.082 1.745.233 1.329-.908 1.918-.722 1.918-.722.381.966.139 1.688.069 1.863.451.489.717 1.117.717 1.885 0 2.701-1.629 3.295-3.189 3.469.254.222.473.641.473 1.304 0 .943-.011 1.7-.011 1.932 0 .187.127.408.474.338a7.025 7.025 0 0 0 4.772-6.67C13.958 3.143 10.827 0 6.979 0";
|
||||
|
||||
interface GitHubIconProps {
|
||||
size: number;
|
||||
fillColor: string;
|
||||
}
|
||||
|
||||
export function GitHubIcon({ size, fillColor }: GitHubIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d={GITHUB_PATH} fill={fillColor} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
const LINKEDIN_PATH =
|
||||
"M.386 2.375A1.35 1.35 0 0 1 0 1.394c0-.39.129-.732.386-.997Q.772.001 1.38 0c.406 0 .725.133.982.397q.386.397.386.997c0 .399-.13.716-.386.98q-.386.398-.982.398c-.397 0-.738-.133-.995-.397m2.146 1.517V12H.216V3.892zm7.712.801Q11 5.602 11 7.19v4.667H8.8V7.519q0-.802-.375-1.245t-1.01-.444q-.633 0-1.009.444T6.031 7.52v4.338H3.818V3.869h2.213v1.06q.336-.53.906-.838t1.28-.307q1.27 0 2.026.908";
|
||||
|
||||
interface LinkedInIconProps {
|
||||
size: number;
|
||||
fillColor: string;
|
||||
}
|
||||
|
||||
export function LinkedInIcon({ size, fillColor }: LinkedInIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 11 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d={LINKEDIN_PATH} fill={fillColor} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
const X_PATH =
|
||||
"m.03 0 4.83 6.46L0 11.713h1.094L5.35 7.115l3.439 4.598h3.723L7.41 4.89 11.935 0H10.84L6.921 4.235 3.754 0zm1.61.806h1.71l7.553 10.101h-1.71z";
|
||||
|
||||
interface XIconProps {
|
||||
size: number;
|
||||
fillColor: string;
|
||||
}
|
||||
|
||||
export function XIcon({ size, fillColor }: XIconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 12 11.8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d={X_PATH} fill={fillColor} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
const FOOTER_SHAPE_PATH =
|
||||
"M0 4a4 4 0 0 1 4-4h344.32c4.197 0 8.369.66 12.361 1.958l49.5 16.084A40 40 0 0 0 422.542 20h517.7c4.293 0 8.559-.691 12.633-2.047l47.785-15.906A40 40 0 0 1 1013.29 0H1356a4 4 0 0 1 4 4v16H0z";
|
||||
|
||||
interface FooterShapeProps {
|
||||
fillColor: string;
|
||||
}
|
||||
|
||||
export function FooterShape({ fillColor }: FooterShapeProps) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
pointerEvents: "none",
|
||||
zIndex: -1,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="100%"
|
||||
height="20"
|
||||
viewBox="0 0 1360 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="none"
|
||||
style={{ display: "block" }}
|
||||
>
|
||||
<path d={FOOTER_SHAPE_PATH} fill={fillColor} />
|
||||
</svg>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 19, // 1px overlap to prevent visual subpixel gaps
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: fillColor,
|
||||
borderBottomLeftRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const BottomGrid = styled.div`
|
||||
display: grid;
|
||||
font-size: ${theme.font.size(3)};
|
||||
gap: ${theme.spacing(6)};
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
`;
|
||||
|
||||
const Copyright = styled.div`
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
font-family: ${theme.font.family.mono};
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
justify-self: start;
|
||||
text-transform: uppercase;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
grid-row: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const Credit = styled.div`
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
font-family: ${theme.font.family.mono};
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
justify-self: end;
|
||||
text-transform: uppercase;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
`;
|
||||
|
||||
type BottomProps = { children?: ReactNode };
|
||||
|
||||
export function Bottom({ children }: BottomProps) {
|
||||
return (
|
||||
<BottomGrid>
|
||||
<Copyright>© 2026 – Twenty</Copyright>
|
||||
{children}
|
||||
<Credit>Design by Ardent.</Credit>
|
||||
</BottomGrid>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Logo as LogoIcon } from '@/icons';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export function Logo() {
|
||||
return (
|
||||
<LogoIcon
|
||||
size={40}
|
||||
fillColor={theme.colors.primary.background[100]}
|
||||
backgroundColor={theme.colors.secondary.background[100]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import { LinkButton } from '@/design-system/components';
|
||||
import { PlusIcon, RectangleFillIcon } from '@/icons';
|
||||
import { FOOTER_NAV_GROUPS } from '@/sections/Footer/constants/footer-nav-groups';
|
||||
import { theme } from '@/theme';
|
||||
import { NavigationMenu } from '@base-ui/react/navigation-menu';
|
||||
import { styled } from '@linaria/react';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
const FooterNav = styled.nav`
|
||||
margin-top: ${theme.spacing(10)};
|
||||
margin-bottom: ${theme.spacing(10)};
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr;
|
||||
}
|
||||
`;
|
||||
|
||||
const NavDivider = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${theme.spacing(1.5)};
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
margin-left: ${theme.spacing(7)};
|
||||
margin-right: ${theme.spacing(7)};
|
||||
}
|
||||
`;
|
||||
|
||||
const NavDividerLine = styled.div`
|
||||
background-color: #1c1c1c1a;
|
||||
flex: 1 1 0%;
|
||||
height: 1px;
|
||||
min-height: 1px;
|
||||
min-width: 0;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
min-width: 1px;
|
||||
width: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
const NavGroup = styled.section`
|
||||
margin-top: ${theme.spacing(4)};
|
||||
margin-bottom: ${theme.spacing(4)};
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
margin-top: 0;
|
||||
margin-bottom: ${theme.spacing(7)};
|
||||
}
|
||||
`;
|
||||
|
||||
const NavGroupTitle = styled.h4`
|
||||
font-size: ${theme.font.size(4)};
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
line-height: 1.35;
|
||||
`;
|
||||
|
||||
const NavMenuList = styled(NavigationMenu.List)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(2)};
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const NavLinkHoverIcon = styled.span`
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
width 0.3s ease-out,
|
||||
opacity 0.3s ease-out;
|
||||
width: 0;
|
||||
`;
|
||||
|
||||
const NavLink = styled(NavigationMenu.Link)`
|
||||
align-items: center;
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
display: flex;
|
||||
font-size: ${theme.font.size(4)};
|
||||
gap: 0;
|
||||
text-decoration: none;
|
||||
transition: gap 0.3s ease-out;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid ${theme.colors.highlight[100]};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
&:hover {
|
||||
gap: ${theme.spacing(2)};
|
||||
}
|
||||
|
||||
&:hover ${NavLinkHoverIcon} {
|
||||
opacity: 1;
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Actions = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(2)};
|
||||
margin-top: ${theme.spacing(8)};
|
||||
|
||||
& > * {
|
||||
width: 126px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function Nav() {
|
||||
return (
|
||||
<NavigationMenu.Root render={<FooterNav />}>
|
||||
{FOOTER_NAV_GROUPS.map((group, index) => (
|
||||
<React.Fragment key={group.id}>
|
||||
{index > 0 && (
|
||||
<NavDivider role="separator">
|
||||
<PlusIcon
|
||||
size={12}
|
||||
strokeColor={theme.colors.highlight[100]}
|
||||
aria-hidden
|
||||
/>
|
||||
<NavDividerLine aria-hidden />
|
||||
<PlusIcon
|
||||
size={12}
|
||||
strokeColor={theme.colors.highlight[100]}
|
||||
aria-hidden
|
||||
/>
|
||||
</NavDivider>
|
||||
)}
|
||||
<NavGroup aria-labelledby={group.id}>
|
||||
<NavGroupTitle id={group.id}>{group.title}</NavGroupTitle>
|
||||
<NavMenuList>
|
||||
{group.links.map((link) => (
|
||||
<NavigationMenu.Item key={link.href + link.label}>
|
||||
<NavLink
|
||||
render={
|
||||
link.external ? (
|
||||
<a
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
) : (
|
||||
<Link href={link.href} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<NavLinkHoverIcon aria-hidden>
|
||||
<RectangleFillIcon
|
||||
size={14}
|
||||
fillColor={theme.colors.secondary.background[100]}
|
||||
/>
|
||||
</NavLinkHoverIcon>
|
||||
{link.label}
|
||||
</NavLink>
|
||||
</NavigationMenu.Item>
|
||||
))}
|
||||
</NavMenuList>
|
||||
{group.showActions && (
|
||||
<Actions>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Talk to us"
|
||||
type="anchor"
|
||||
variant="contained"
|
||||
/>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Get started"
|
||||
type="anchor"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Actions>
|
||||
)}
|
||||
</NavGroup>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</NavigationMenu.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Container } from '@/design-system/components';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { FooterShape } from '../../FooterShape';
|
||||
|
||||
const FooterRoot = styled.footer`
|
||||
background-color: ${theme.colors.secondary.background[100]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const FooterContainer = styled(Container)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 1280px;
|
||||
position: relative;
|
||||
padding-bottom: ${theme.spacing(4)};
|
||||
padding-left: ${theme.spacing(4)};
|
||||
padding-right: ${theme.spacing(4)};
|
||||
|
||||
@media (min-width: ${theme.breakpoints.lg}px) {
|
||||
padding-bottom: ${theme.spacing(10)};
|
||||
padding-left: ${theme.spacing(10)};
|
||||
padding-right: ${theme.spacing(10)};
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterBackground = styled.div`
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
`;
|
||||
|
||||
const FooterContent = styled.div`
|
||||
margin-top: auto;
|
||||
padding-bottom: ${theme.spacing(12)};
|
||||
padding-top: ${theme.spacing(12)};
|
||||
padding-left: ${theme.spacing(4)};
|
||||
padding-right: ${theme.spacing(4)};
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
padding-left: ${theme.spacing(20)};
|
||||
padding-right: ${theme.spacing(20)};
|
||||
padding-top: ${theme.spacing(20)};
|
||||
}
|
||||
|
||||
@media (min-width: ${theme.breakpoints.lg}px) {
|
||||
padding-left: ${theme.spacing(30)};
|
||||
padding-right: ${theme.spacing(30)};
|
||||
}
|
||||
`;
|
||||
|
||||
type RootProps = { children: ReactNode };
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<FooterRoot>
|
||||
<FooterContainer>
|
||||
<FooterBackground aria-hidden />
|
||||
<FooterContent>
|
||||
<FooterShape fillColor={theme.colors.primary.background[100]} />
|
||||
{children}
|
||||
</FooterContent>
|
||||
</FooterContainer>
|
||||
</FooterRoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { ArrowRightUpIcon } from '@/icons';
|
||||
import { SOCIAL_LINKS } from '@/sections/Footer/constants/social-links';
|
||||
import { theme } from '@/theme';
|
||||
import { Separator } from '@base-ui/react/separator';
|
||||
import { styled } from '@linaria/react';
|
||||
import React from 'react';
|
||||
|
||||
const SocialContainer = styled.nav`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${theme.spacing(6)};
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
grid-column: 2;
|
||||
justify-self: end;
|
||||
}
|
||||
`;
|
||||
|
||||
const SocialDivider = styled(Separator)`
|
||||
background-color: ${theme.colors.primary.border[40]};
|
||||
border: none;
|
||||
height: 10px;
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
const SocialItem = styled.a`
|
||||
align-items: center;
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
display: flex;
|
||||
font-size: ${theme.font.size(3)};
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
gap: ${theme.spacing(3)};
|
||||
line-height: 14px;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid ${theme.colors.highlight[100]};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function Social() {
|
||||
return (
|
||||
<SocialContainer aria-label="Social media">
|
||||
{SOCIAL_LINKS.filter((item) => item.showInDrawer).map((item, index) => (
|
||||
<React.Fragment key={item.href}>
|
||||
{index > 0 && <SocialDivider orientation="vertical" />}
|
||||
<SocialItem
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={item.ariaLabel}
|
||||
>
|
||||
<item.icon
|
||||
size={14}
|
||||
fillColor={theme.colors.secondary.background[100]}
|
||||
aria-hidden
|
||||
/>
|
||||
{item.label}
|
||||
{item.label != null && (
|
||||
<ArrowRightUpIcon
|
||||
size={8}
|
||||
strokeColor={theme.colors.highlight[100]}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
</SocialItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</SocialContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Bottom } from './Bottom/Bottom';
|
||||
import { Logo } from './Logo/Logo';
|
||||
import { Nav } from './Nav/Nav';
|
||||
import { Root } from './Root/Root';
|
||||
import { Social } from './Social/Social';
|
||||
|
||||
export const Footer = { Root, Logo, Nav, Social, Bottom };
|
||||
@@ -0,0 +1,47 @@
|
||||
export const FOOTER_NAV_GROUPS = [
|
||||
{
|
||||
id: "footer-sitemap",
|
||||
title: "Sitemap",
|
||||
showActions: false,
|
||||
links: [
|
||||
{ label: "Home", href: "/", external: false },
|
||||
{ label: "Product", href: "/product", external: false },
|
||||
{ label: "Pricing", href: "/pricing", external: false },
|
||||
{ label: "Partners", href: "/partner", external: false },
|
||||
{ label: "Why Twenty", href: "/why-twenty", external: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "footer-help",
|
||||
title: "Help",
|
||||
showActions: false,
|
||||
links: [
|
||||
{ label: "Developers", href: "#", external: false },
|
||||
{ label: "FAQ", href: "#", external: false },
|
||||
{ label: "Support", href: "#", external: false },
|
||||
{ label: "Release Notes", href: "#", external: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "footer-legal",
|
||||
title: "Legal",
|
||||
showActions: false,
|
||||
links: [
|
||||
{ label: "Cookie Policy", href: "#", external: false },
|
||||
{ label: "Privacy Policy", href: "#", external: false },
|
||||
{ label: "Terms and Conditions", href: "#", external: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "footer-connect",
|
||||
title: "Connect",
|
||||
showActions: true,
|
||||
links: [
|
||||
{
|
||||
label: "LinkedIn",
|
||||
href: "https://www.linkedin.com/company/twenty",
|
||||
external: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
export const NAV_ITEMS = [
|
||||
{ label: 'Product', href: '/product' },
|
||||
{ label: 'Pricing', href: '/pricing' },
|
||||
{ label: 'Partners', href: '/partner' },
|
||||
{ label: 'Why Twenty', href: '/why-twenty' },
|
||||
];
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DiscordIcon, GitHubIcon, LinkedInIcon, XIcon } from "@/icons"
|
||||
|
||||
export const SOCIAL_LINKS = [
|
||||
{
|
||||
href: "https://github.com/twentyhq/twenty",
|
||||
icon: GitHubIcon,
|
||||
label: "39.8K",
|
||||
ariaLabel: "GitHub (opens in new tab)",
|
||||
showInDesktop: true,
|
||||
showInDrawer: true,
|
||||
},
|
||||
{
|
||||
href: "https://discord.gg/cx5n4Jzs57",
|
||||
icon: DiscordIcon,
|
||||
label: "5.6K",
|
||||
ariaLabel: "Discord (opens in new tab)",
|
||||
className: "discord-link",
|
||||
showInDesktop: true,
|
||||
showInDrawer: true,
|
||||
},
|
||||
{
|
||||
href: "https://www.linkedin.com/company/twenty",
|
||||
icon: LinkedInIcon,
|
||||
label: undefined,
|
||||
ariaLabel: "LinkedIn (opens in new tab)",
|
||||
showInDesktop: false,
|
||||
showInDrawer: true,
|
||||
},
|
||||
{
|
||||
href: "https://x.com/twentycrm",
|
||||
icon: XIcon,
|
||||
label: undefined,
|
||||
ariaLabel: "X (opens in new tab)",
|
||||
showInDesktop: false,
|
||||
showInDrawer: true,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
Body as BaseBody,
|
||||
type BodyProps,
|
||||
} from '@/design-system/components/Body/Body';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const BodyContainer = styled.div`
|
||||
color: ${theme.colors.primary.text[60]};
|
||||
margin-top: calc(${theme.spacing(2)} - ${theme.spacing(6)});
|
||||
width: 360px;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
&[data-page=${Pages.Home}] {
|
||||
width: 591px;
|
||||
}
|
||||
|
||||
&[data-page=${Pages.Partner}] {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
&[data-page=${Pages.Pricing}] {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
&[data-page=${Pages.Product}] {
|
||||
width: 591px;
|
||||
}
|
||||
|
||||
&[data-page=${Pages.WhyTwenty}] {
|
||||
width: 443px;
|
||||
color: ${theme.colors.secondary.text[100]};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type HeroBodyProps = BodyProps & { page: Pages };
|
||||
|
||||
export function Body({ page, ...bodyProps }: HeroBodyProps) {
|
||||
return (
|
||||
<BodyContainer data-page={page}>
|
||||
<BaseBody {...bodyProps} />
|
||||
</BodyContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
const CTAsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${theme.spacing(4)};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export type HeroCtaProps = { children: ReactNode };
|
||||
|
||||
export function Cta({ children }: HeroCtaProps) {
|
||||
return <CTAsContainer>{children}</CTAsContainer>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
Heading as BaseHeading,
|
||||
type HeadingProps,
|
||||
} from '@/design-system/components/Heading/Heading';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const HeadingContainer = styled.div`
|
||||
width: 360px;
|
||||
word-wrap: break-word;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
width: 672px;
|
||||
|
||||
&[data-page=${Pages.WhyTwenty}] {
|
||||
color: ${theme.colors.secondary.text[100]};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type HeroHeadingProps = HeadingProps & { page: Pages };
|
||||
|
||||
export function Heading({
|
||||
page,
|
||||
size = 'lg',
|
||||
weight = 'light',
|
||||
...props
|
||||
}: HeroHeadingProps) {
|
||||
return (
|
||||
<HeadingContainer data-page={page}>
|
||||
<BaseHeading size={size} weight={weight} {...props} />
|
||||
</HeadingContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Image } from '@/design-system/components';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledHomeVisual = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
margin-top: ${theme.spacing(5)};
|
||||
position: relative;
|
||||
right: calc(${theme.spacing(2)} - ${theme.spacing(6)});
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
height: 500px;
|
||||
margin-top: ${theme.spacing(17.5)};
|
||||
overflow: hidden;
|
||||
right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledBackgroundImage = styled(Image)`
|
||||
grid-area: 1 / 1;
|
||||
justify-self: end;
|
||||
width: 98%;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
width: 850px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledForegroundImage = styled(Image)`
|
||||
grid-area: 1 / 1;
|
||||
justify-self: end;
|
||||
margin-right: ${theme.spacing(6.5)};
|
||||
margin-top: ${theme.spacing(20)};
|
||||
width: 98%;
|
||||
z-index: 1;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
justify-self: center;
|
||||
margin-right: ${theme.spacing(40)};
|
||||
margin-top: ${theme.spacing(17.5)};
|
||||
width: 800px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function HomeVisual() {
|
||||
return (
|
||||
<StyledHomeVisual>
|
||||
<StyledBackgroundImage src="/images/home/hero/background.png" alt="" />
|
||||
<StyledForegroundImage src="/images/home/hero/foreground.png" alt="" />
|
||||
</StyledHomeVisual>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { IllustrationType } from '@/design-system/components/Illustration/types/Illustration';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledIllustrationContainer = styled.div`
|
||||
margin-top: ${theme.spacing(6)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type IllustrationProps = IllustrationType & {
|
||||
backgroundColor: string;
|
||||
};
|
||||
|
||||
export function Illustration({
|
||||
src,
|
||||
title,
|
||||
backgroundColor,
|
||||
}: IllustrationProps) {
|
||||
return (
|
||||
<StyledIllustrationContainer>
|
||||
<iframe
|
||||
src={src}
|
||||
title={title}
|
||||
allow="clipboard-write; encrypted-media; gyroscope; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '462px',
|
||||
border: 'none',
|
||||
backgroundColor: backgroundColor,
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
</StyledIllustrationContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Container } from '@/design-system/components';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const StyledSection = styled.section`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled(Container)`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
padding-top: ${theme.spacing(7.5)};
|
||||
padding-left: ${theme.spacing(4)};
|
||||
padding-right: ${theme.spacing(4)};
|
||||
row-gap: ${theme.spacing(6)};
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
padding-top: ${theme.spacing(12)};
|
||||
padding-left: ${theme.spacing(10)};
|
||||
padding-right: ${theme.spacing(10)};
|
||||
}
|
||||
`;
|
||||
|
||||
type RootProps = { backgroundColor: string; children: ReactNode };
|
||||
|
||||
export function Root({ backgroundColor, children }: RootProps) {
|
||||
return (
|
||||
<StyledSection style={{ backgroundColor }}>
|
||||
<StyledContainer>{children}</StyledContainer>
|
||||
</StyledSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Image } from '@/design-system/components';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const VisualContainer = styled.div`
|
||||
border-radius: ${theme.radius(1)};
|
||||
height: 462px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledBackground = styled(Image)`
|
||||
aspect-ratio: auto;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledIframe = styled.iframe`
|
||||
border: none;
|
||||
height: 200%;
|
||||
mix-blend-mode: lighten;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 51.5%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 200%;
|
||||
`;
|
||||
|
||||
export function WhyTwentyVisual() {
|
||||
return (
|
||||
<VisualContainer>
|
||||
<StyledBackground src="/images/why-twenty/hero/background.png" alt="" />
|
||||
<StyledIframe
|
||||
src="https://app.endlesstools.io/embed/479566da-0f55-44ef-99fe-90350aa88b9c"
|
||||
title="Endless Tools Editor"
|
||||
allow="clipboard-write; encrypted-media; gyroscope; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
/>
|
||||
</VisualContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Body } from './Body/Body';
|
||||
import { Cta } from './Cta/Cta';
|
||||
import { Heading } from './Heading/Heading';
|
||||
import { HomeVisual } from './HomeVisual/HomeVisual';
|
||||
import { Illustration } from './Illustration/Illustration';
|
||||
import { Root } from './Root/Root';
|
||||
import { WhyTwentyVisual } from './WhyTwentyVisual/WhyTwentyVisual';
|
||||
|
||||
export const Hero = {
|
||||
Root,
|
||||
Heading,
|
||||
Body,
|
||||
Cta,
|
||||
Illustration,
|
||||
HomeVisual,
|
||||
WhyTwentyVisual,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { BodyType } from '@/design-system/components/Body/types/Body';
|
||||
import { HeadingType } from '@/design-system/components/Heading/types/Heading';
|
||||
|
||||
export type HeroDataType = {
|
||||
heading: HeadingType[];
|
||||
body: BodyType;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type { HeroDataType } from './HeroData';
|
||||
@@ -0,0 +1,34 @@
|
||||
import { LinkButton } from '@/design-system/components';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const CtaContainer = styled.div`
|
||||
display: none;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${theme.spacing(2)};
|
||||
}
|
||||
`;
|
||||
|
||||
export function Cta() {
|
||||
return (
|
||||
<CtaContainer>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Log in"
|
||||
type="anchor"
|
||||
variant="outlined"
|
||||
/>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Get started"
|
||||
type="anchor"
|
||||
variant="contained"
|
||||
/>
|
||||
</CtaContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { LinkButton } from '@/design-system/components';
|
||||
import { ArrowRightUpIcon } from '@/icons';
|
||||
import { NAV_ITEMS } from '@/sections/Menu/constants/nav-items';
|
||||
import { SOCIAL_LINKS } from '@/sections/Menu/constants/social-links';
|
||||
import { theme } from '@/theme';
|
||||
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 React from 'react';
|
||||
|
||||
const DrawerPopup = styled(Drawer.Popup)`
|
||||
background: ${theme.colors.primary.background[100]};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
overflow-y: auto;
|
||||
padding-bottom: ${theme.spacing(4)};
|
||||
padding-left: ${theme.spacing(4)};
|
||||
padding-right: ${theme.spacing(4)};
|
||||
padding-top: ${theme.spacing(35)};
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
z-index: 90;
|
||||
`;
|
||||
|
||||
const NavigationContainer = styled.nav`
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(8)};
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const NavItem = styled(Link)`
|
||||
border-radius: ${theme.radius(2)};
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
display: block;
|
||||
font-family: ${theme.font.family.mono};
|
||||
font-size: ${theme.font.size(8)};
|
||||
font-weight: ${theme.font.weight.light};
|
||||
letter-spacing: 0;
|
||||
line-height: 38px;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
width: 100%;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid ${theme.colors.highlight[100]};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HorizontalSeparator = styled(Separator)`
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
${theme.colors.primary.border[60]} 0,
|
||||
${theme.colors.primary.border[60]} 1px,
|
||||
transparent 2px,
|
||||
transparent 4px
|
||||
);
|
||||
border: none;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const CtaContainer = styled.div`
|
||||
margin-bottom: ${theme.spacing(10)};
|
||||
`;
|
||||
|
||||
const SocialContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const SocialItem = styled.a`
|
||||
align-items: center;
|
||||
border-radius: ${theme.radius(1)};
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
display: flex;
|
||||
font-size: ${theme.font.size(3)};
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
gap: ${theme.spacing(3)};
|
||||
line-height: 14px;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid ${theme.colors.highlight[100]};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Divider = styled(Separator)`
|
||||
border-left: 1px solid ${theme.colors.primary.border[40]};
|
||||
height: 10px;
|
||||
width: 0px;
|
||||
`;
|
||||
|
||||
export function MenuDrawer() {
|
||||
return (
|
||||
<Drawer.Portal>
|
||||
<DrawerPopup aria-label="Navigation menu">
|
||||
<NavigationContainer aria-label="Mobile navigation">
|
||||
{NAV_ITEMS.map((item, index) => (
|
||||
<React.Fragment key={item.href}>
|
||||
<Drawer.Close
|
||||
nativeButton={false}
|
||||
render={<NavItem href={item.href} />}
|
||||
>
|
||||
{item.label}
|
||||
</Drawer.Close>
|
||||
{index < NAV_ITEMS.length - 1 && (
|
||||
<HorizontalSeparator orientation="horizontal" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</NavigationContainer>
|
||||
|
||||
<CtaContainer>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Log in"
|
||||
type="anchor"
|
||||
variant="outlined"
|
||||
/>
|
||||
</CtaContainer>
|
||||
|
||||
<SocialContainer>
|
||||
{SOCIAL_LINKS.filter((item) => item.showInDrawer).map(
|
||||
(item, index) => (
|
||||
<React.Fragment key={item.href}>
|
||||
{index > 0 && <Divider orientation="vertical" />}
|
||||
<SocialItem
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={item.ariaLabel}
|
||||
>
|
||||
<item.icon
|
||||
size={14}
|
||||
fillColor={theme.colors.secondary.background[100]}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.label}
|
||||
{item.label && (
|
||||
<ArrowRightUpIcon
|
||||
size={8}
|
||||
strokeColor={theme.colors.highlight[100]}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</SocialItem>
|
||||
</React.Fragment>
|
||||
),
|
||||
)}
|
||||
</SocialContainer>
|
||||
</DrawerPopup>
|
||||
</Drawer.Portal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Logo as LogoIcon } from '@/icons';
|
||||
import { theme } from '@/theme';
|
||||
import { Drawer } from '@base-ui/react/drawer';
|
||||
import { styled } from '@linaria/react';
|
||||
import Link from 'next/link';
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const LogoLink = styled(Link)`
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid ${theme.colors.highlight[100]};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function Logo() {
|
||||
return (
|
||||
<LogoContainer>
|
||||
<Drawer.Close
|
||||
nativeButton={false}
|
||||
render={<LogoLink href="/" aria-label="Home" />}
|
||||
>
|
||||
<LogoIcon
|
||||
size={40}
|
||||
fillColor={theme.colors.primary.background[100]}
|
||||
backgroundColor={theme.colors.secondary.background[100]}
|
||||
/>
|
||||
</Drawer.Close>
|
||||
</LogoContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { NAV_ITEMS } from '@/sections/Menu/constants/nav-items';
|
||||
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 React from 'react';
|
||||
|
||||
const NavList = styled(NavigationMenu.List)`
|
||||
display: none;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${theme.spacing(8)};
|
||||
}
|
||||
`;
|
||||
|
||||
const NavLink = styled(NavigationMenu.Link)`
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
font-family: ${theme.font.family.mono};
|
||||
font-size: ${theme.font.size(3)};
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
letter-spacing: 0;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid ${theme.colors.highlight[100]};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Divider = styled(Separator)`
|
||||
border-left: 1px solid ${theme.colors.primary.border[40]};
|
||||
height: 10px;
|
||||
width: 0px;
|
||||
`;
|
||||
|
||||
export function Nav() {
|
||||
return (
|
||||
<NavigationMenu.Root render={<div />}>
|
||||
<NavList>
|
||||
{NAV_ITEMS.map((item, index) => (
|
||||
<React.Fragment key={item.href}>
|
||||
<NavigationMenu.Item>
|
||||
<NavLink render={<Link href={item.href} />}>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
</NavigationMenu.Item>
|
||||
{index < NAV_ITEMS.length - 1 && (
|
||||
<Divider orientation="vertical" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</NavList>
|
||||
</NavigationMenu.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { Container, IconButton, LinkButton } from '@/design-system/components';
|
||||
import { CloseIcon, MenuIcon } from '@/icons';
|
||||
import { theme } from '@/theme';
|
||||
import { Drawer } from '@base-ui/react/drawer';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { CloseDrawerWhenNavigationExpandsEffect } from '../../effect-components/CloseDrawerWhenNavigationExpandsEffect';
|
||||
import { MenuDrawer } from '../Drawer/Drawer';
|
||||
|
||||
const StyledMenuContainer = styled(Container)`
|
||||
margin-top: ${theme.spacing(4)};
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
const StyledNav = styled.nav`
|
||||
align-items: center;
|
||||
background-color: ${theme.colors.primary.background[100]};
|
||||
border-radius: ${theme.radius(2)};
|
||||
display: flex;
|
||||
height: 48px;
|
||||
justify-content: space-between;
|
||||
padding-left: ${theme.spacing(4)};
|
||||
padding-right: ${theme.spacing(4)};
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.lg}px) {
|
||||
padding-left: ${theme.spacing(10)};
|
||||
padding-right: ${theme.spacing(10)};
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileRightContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${theme.spacing(1)};
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
type RootProps = { children: ReactNode };
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Drawer.Root
|
||||
open={isDrawerOpen}
|
||||
onOpenChange={setIsDrawerOpen}
|
||||
swipeDirection="down"
|
||||
>
|
||||
<CloseDrawerWhenNavigationExpandsEffect
|
||||
onClose={() => setIsDrawerOpen(false)}
|
||||
/>
|
||||
<StyledMenuContainer>
|
||||
<StyledNav aria-label="Primary navigation">
|
||||
{children}
|
||||
<MobileRightContainer>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Get started"
|
||||
type="anchor"
|
||||
variant="contained"
|
||||
/>
|
||||
<IconButton
|
||||
icon={isDrawerOpen ? CloseIcon : MenuIcon}
|
||||
ariaLabel={isDrawerOpen ? 'Close menu' : 'Open menu'}
|
||||
borderColor={theme.colors.primary.border[20]}
|
||||
iconFillColor="none"
|
||||
iconSize={14}
|
||||
iconStrokeColor={theme.colors.primary.text[100]}
|
||||
size={40}
|
||||
onClick={() => setIsDrawerOpen((prev) => !prev)}
|
||||
/>
|
||||
</MobileRightContainer>
|
||||
</StyledNav>
|
||||
</StyledMenuContainer>
|
||||
<MenuDrawer />
|
||||
</Drawer.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ArrowRightUpIcon } from '@/icons';
|
||||
import { SOCIAL_LINKS } from '@/sections/Menu/constants/social-links';
|
||||
import { theme } from '@/theme';
|
||||
import { Separator } from '@base-ui/react/separator';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const SocialContainer = styled.nav`
|
||||
display: none;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${theme.spacing(5)};
|
||||
}
|
||||
`;
|
||||
|
||||
const SocialLinkItem = styled.div`
|
||||
display: none;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${theme.spacing(5)};
|
||||
|
||||
&.discord-link {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ${theme.breakpoints.lg}px) {
|
||||
&.discord-link {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const SocialLink = styled.a`
|
||||
align-items: center;
|
||||
border-radius: ${theme.radius(1)};
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
display: flex;
|
||||
font-size: ${theme.font.size(3)};
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
gap: ${theme.spacing(2)};
|
||||
letter-spacing: 0;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid ${theme.colors.highlight[100]};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Divider = styled(Separator)`
|
||||
border-left: 1px solid ${theme.colors.primary.border[40]};
|
||||
height: 10px;
|
||||
width: 0px;
|
||||
`;
|
||||
|
||||
export function Social() {
|
||||
return (
|
||||
<SocialContainer aria-label="Social media">
|
||||
{SOCIAL_LINKS.filter((item) => item.showInDesktop).map((item, index) => (
|
||||
<SocialLinkItem key={item.href} className={item.className}>
|
||||
{index > 0 && <Divider orientation="vertical" />}
|
||||
<SocialLink
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={item.ariaLabel}
|
||||
>
|
||||
<item.icon
|
||||
size={14}
|
||||
fillColor={theme.colors.secondary.background[100]}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.label}
|
||||
<ArrowRightUpIcon
|
||||
size={8}
|
||||
strokeColor={theme.colors.highlight[100]}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</SocialLink>
|
||||
</SocialLinkItem>
|
||||
))}
|
||||
</SocialContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Cta } from './Cta/Cta';
|
||||
import { Logo } from './Logo/Logo';
|
||||
import { Nav } from './Nav/Nav';
|
||||
import { Root } from './Root/Root';
|
||||
import { Social } from './Social/Social';
|
||||
|
||||
export const Menu = { Root, Logo, Nav, Social, Cta };
|
||||
@@ -0,0 +1,6 @@
|
||||
export const NAV_ITEMS = [
|
||||
{ label: 'Product', href: '/product' },
|
||||
{ label: 'Pricing', href: '/pricing' },
|
||||
{ label: 'Partners', href: '/partner' },
|
||||
{ label: 'Why Twenty', href: '/why-twenty' },
|
||||
];
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DiscordIcon, GitHubIcon, LinkedInIcon, XIcon } from "@/icons"
|
||||
|
||||
export const SOCIAL_LINKS = [
|
||||
{
|
||||
href: "https://github.com/twentyhq/twenty",
|
||||
icon: GitHubIcon,
|
||||
label: "39.8K",
|
||||
ariaLabel: "GitHub (opens in new tab)",
|
||||
showInDesktop: true,
|
||||
showInDrawer: true,
|
||||
},
|
||||
{
|
||||
href: "https://discord.gg/cx5n4Jzs57",
|
||||
icon: DiscordIcon,
|
||||
label: "5.6K",
|
||||
ariaLabel: "Discord (opens in new tab)",
|
||||
className: "discord-link",
|
||||
showInDesktop: true,
|
||||
showInDrawer: true,
|
||||
},
|
||||
{
|
||||
href: "https://www.linkedin.com/company/twenty",
|
||||
icon: LinkedInIcon,
|
||||
label: undefined,
|
||||
ariaLabel: "LinkedIn (opens in new tab)",
|
||||
showInDesktop: false,
|
||||
showInDrawer: true,
|
||||
},
|
||||
{
|
||||
href: "https://x.com/twentycrm",
|
||||
icon: XIcon,
|
||||
label: undefined,
|
||||
ariaLabel: "X (opens in new tab)",
|
||||
showInDesktop: false,
|
||||
showInDrawer: true,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { theme } from "@/theme";
|
||||
|
||||
interface CloseDrawerWhenNavigationExpandsEffectProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to viewport changes and calls onClose whenever the layout
|
||||
* crosses into the desktop breakpoint, so the drawer closes when the full
|
||||
* navigation is shown.
|
||||
*/
|
||||
export function CloseDrawerWhenNavigationExpandsEffect({
|
||||
onClose,
|
||||
}: CloseDrawerWhenNavigationExpandsEffectProps) {
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia(`(min-width: ${theme.breakpoints.md}px)`);
|
||||
|
||||
const handleChange = () => {
|
||||
if (mediaQuery.matches) onClose();
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
}, [onClose]);
|
||||
|
||||
return null;
|
||||
}
|
||||