diff --git a/packages/twenty-website/public/illustrations/common/glass-environment.jpg b/packages/twenty-website/public/illustrations/common/glass-environment.jpg deleted file mode 100644 index 0ddd056788..0000000000 Binary files a/packages/twenty-website/public/illustrations/common/glass-environment.jpg and /dev/null differ diff --git a/packages/twenty-website/public/illustrations/generated/handshake.png b/packages/twenty-website/public/illustrations/generated/handshake.png deleted file mode 100644 index bc5639b8fa..0000000000 Binary files a/packages/twenty-website/public/illustrations/generated/handshake.png and /dev/null differ diff --git a/packages/twenty-website/public/illustrations/generated/home-background-wheat.webp b/packages/twenty-website/public/illustrations/generated/home-background-wheat.webp new file mode 100644 index 0000000000..b26530e714 Binary files /dev/null and b/packages/twenty-website/public/illustrations/generated/home-background-wheat.webp differ diff --git a/packages/twenty-website/public/illustrations/generated/mic.png b/packages/twenty-website/public/illustrations/generated/mic.png deleted file mode 100644 index 94ba935b71..0000000000 Binary files a/packages/twenty-website/public/illustrations/generated/mic.png and /dev/null differ diff --git a/packages/twenty-website/public/images/home/hero/apple-workspace-logo.svg b/packages/twenty-website/public/images/home/hero/apple-workspace-logo.svg new file mode 100644 index 0000000000..dd2dba3161 --- /dev/null +++ b/packages/twenty-website/public/images/home/hero/apple-workspace-logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/twenty-website/public/images/home/logo-bar/windmill-original.webp b/packages/twenty-website/public/images/home/logo-bar/windmill-original.webp deleted file mode 100644 index 75e39b2a74..0000000000 Binary files a/packages/twenty-website/public/images/home/logo-bar/windmill-original.webp and /dev/null differ diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeHero.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeHero.tsx index d9a77f5714..c0c1583be3 100644 --- a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeHero.tsx +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeHero.tsx @@ -2,11 +2,10 @@ import { msg } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { styled } from '@linaria/react'; -import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data'; import { HeadingPart, LinkButton } from '@/design-system/components'; import { getServerI18n } from '@/lib/i18n/server'; import { WebGlMount } from '@/lib/visual-runtime'; -import { AppPreview } from '@/sections/AppPreview'; +import { APP_PREVIEW_DATA, AppPreview } from '@/sections/AppPreview'; import { TalkToUsButton } from '@/sections/ContactCal'; import { HeroBody, HeroCta, HeroHeading, HeroSection } from '@/templates/Hero'; import { HomeBackgroundHalftone } from '@/sections/Hero/visuals/components/HomeBackgroundHalftone'; diff --git a/packages/twenty-website/src/app/[locale]/(home)/app-preview.data.ts b/packages/twenty-website/src/app/[locale]/(home)/app-preview.data.ts deleted file mode 100644 index 1d8b636fe9..0000000000 --- a/packages/twenty-website/src/app/[locale]/(home)/app-preview.data.ts +++ /dev/null @@ -1,1629 +0,0 @@ -import type { - AppPreviewConfig, - DashboardData, - DashboardPageDefinition, - KanbanPageDefinition, - TablePageDefinition, -} from '@/sections/AppPreview'; -import { SHARED_PEOPLE_AVATAR_URLS } from '@/content/site/asset-paths'; - -const PEOPLE_AVATAR_URLS = SHARED_PEOPLE_AVATAR_URLS; - -const SALES_DASHBOARD_DATA: DashboardData = { - metrics: [ - { - id: 'new-subscriptions', - title: 'New subscriptions', - value: '2,432', - }, - { - id: 'churn', - title: 'Churn', - value: '161', - }, - { - id: 'churn-rate', - title: 'Churn %', - value: '6.6%', - }, - ], - visitsChart: { - alt: 'Visits bar chart comparing this year and last year', - height: 1116, - src: '/images/home/hero/sales-dashboard/visits.webp', - width: 2316, - }, - revenueChart: { - alt: 'Revenue line chart by month', - height: 1116, - src: '/images/home/hero/sales-dashboard/revenue.webp', - width: 2316, - }, - distributionChart: { - alt: 'Ultra versus Plus donut chart distribution', - height: 1116, - src: '/images/home/hero/sales-dashboard/distribution.webp', - width: 756, - }, -}; - -const SALES_DASHBOARD_PAGE: DashboardPageDefinition = { - type: 'dashboard', - header: { - title: 'Sales Dashboard', - }, - dashboard: SALES_DASHBOARD_DATA, -}; - -const OPPORTUNITY_KANBAN_PAGE: KanbanPageDefinition = { - type: 'kanban', - header: { - title: 'Best leads', - }, - lanes: [ - { - id: 'identified', - label: 'Identified', - tone: 'pink', - cards: [ - { - id: 'anthropic-enterprise-expansion', - title: 'Enterprise Expansion', - amount: '$500,000', - company: { - type: 'entity', - name: 'Anthropic', - domain: 'anthropic.com', - }, - accountOwner: { - type: 'person', - name: 'Dario Amodei', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei, - }, - rating: 2, - date: 'Jul 1, 2023', - mainContact: { - type: 'person', - name: 'Dario Amodei', - shortLabel: 'D', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei, - }, - recordId: 'OPP-1', - }, - { - id: 'figma-ai-prototyping', - title: 'AI Prototyping', - amount: '$3,500,000', - company: { type: 'entity', name: 'Figma', domain: 'figma.com' }, - accountOwner: { - type: 'person', - name: 'Dylan Field', - tone: 'purple', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.dylanField, - }, - rating: 2, - date: 'Jul 12, 2023', - mainContact: { - type: 'person', - name: 'Dylan Field', - shortLabel: 'D', - tone: 'purple', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.dylanField, - }, - recordId: 'OPP-2', - }, - ], - }, - { - id: 'qualified', - label: 'Qualified', - tone: 'purple', - cards: [ - { - id: 'notion-workspace-consolidation', - title: 'Workspace Consolidation', - amount: '$750,000', - company: { type: 'entity', name: 'Notion', domain: 'notion.com' }, - accountOwner: { - type: 'person', - name: 'Ivan Zhao', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao, - }, - rating: 4, - date: 'Jul 8, 2023', - mainContact: { - type: 'person', - name: 'Ivan Zhao', - shortLabel: 'I', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao, - }, - recordId: 'OPP-3', - }, - ], - }, - { - id: 'engaged', - label: 'Engaged', - tone: 'blue', - cards: [ - { - id: 'github-copilot-rollout', - title: 'Copilot Rollout', - amount: '$900,000', - company: { type: 'entity', name: 'Github', domain: 'github.com' }, - accountOwner: { - type: 'person', - name: 'Chris Wanstrath', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.chrisWanstrath, - }, - rating: 3, - date: 'Jul 14, 2023', - mainContact: { - type: 'person', - name: 'Thomas Dohmke', - shortLabel: 'T', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.thomasDohmke, - }, - recordId: 'OPP-4', - }, - { - id: 'stripe-billing-expansion', - title: 'Billing Expansion', - amount: '$1,800,000', - company: { type: 'entity', name: 'Stripe', domain: 'stripe.com' }, - accountOwner: { - type: 'person', - name: 'Patrick Collison', - tone: 'blue', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison, - }, - rating: 5, - date: 'Jul 17, 2023', - mainContact: { - type: 'person', - name: 'Patrick Collison', - shortLabel: 'P', - tone: 'blue', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison, - }, - recordId: 'OPP-5', - }, - { - id: 'airbnb-host-ops', - title: 'Host Ops', - amount: '$4,200,000', - company: { type: 'entity', name: 'Airbnb', domain: 'airbnb.com' }, - accountOwner: { - type: 'person', - name: 'Joe Gebbia', - tone: 'pink', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.joeGebbia, - }, - rating: 3, - date: 'Jul 15, 2023', - mainContact: { - type: 'person', - name: 'Brian Chesky', - shortLabel: 'B', - tone: 'pink', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.brianChesky, - }, - recordId: 'OPP-6', - }, - ], - }, - { - id: 'proposed', - label: 'Proposed', - tone: 'green', - cards: [], - }, - { - id: 'no-value', - label: 'No Value', - tone: 'gray', - cards: [ - { - id: 'mailchimp-lifecycle-campaigns', - title: 'Lifecycle Campaigns', - amount: '$1,250,000', - company: { - type: 'entity', - name: 'Mailchimp', - domain: 'mailchimp.com', - }, - accountOwner: { - type: 'person', - name: 'Ben Chestnut', - tone: 'amber', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.benChestnut, - }, - rating: 4, - date: 'Jul 23, 2023', - mainContact: { - type: 'person', - name: 'Rania Succar', - shortLabel: 'R', - tone: 'amber', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.anonymousLaura, - }, - recordId: 'OPP-7', - }, - ], - }, - ], -}; - -function createTablePage({ - title, - count, - columns, - rows, -}: { - title: string; - count: number; - columns: TablePageDefinition['columns']; - rows: TablePageDefinition['rows']; -}): TablePageDefinition { - return { - type: 'table', - header: { - title, - count, - }, - columns, - rows, - }; -} - -export const APP_PREVIEW_DATA: { visual: AppPreviewConfig } = { - visual: { - workspace: { icon: 'apple', name: 'Apple' }, - tableWidth: 1700, - actions: ['Filter', 'Sort', 'Options'], - favoritesNav: [ - { - id: 'sales-dashboard', - label: 'Sales Dashboard', - icon: { kind: 'avatar', label: 'S', tone: 'amber', shape: 'circle' }, - meta: 'Dashboard', - page: SALES_DASHBOARD_PAGE, - }, - ], - workspaceNav: [ - { - id: 'companies', - label: 'Companies', - icon: { kind: 'tabler', name: 'buildingSkyscraper', tone: 'blue' }, - active: true, - page: createTablePage({ - title: 'All Companies', - count: 9, - columns: [ - { - id: 'company', - label: 'Companies', - width: 180, - isFirstColumn: true, - }, - { id: 'url', label: 'Url', width: 140 }, - { id: 'createdBy', label: 'Created By', width: 150 }, - { id: 'address', label: 'Address', width: 120 }, - { id: 'accountOwner', label: 'Account Owner', width: 150 }, - { id: 'icp', label: 'ICP', width: 80 }, - { id: 'arr', label: 'ARR', width: 120, align: 'right' }, - { id: 'linkedin', label: 'Linkedin', width: 96 }, - { id: 'industry', label: 'Industry', width: 96 }, - { id: 'mainContact', label: 'Main contact', width: 120 }, - { id: 'employees', label: 'Employees', width: 120, align: 'right' }, - { id: 'opportunities', label: 'Opportunities', width: 122 }, - { id: 'added', label: 'Added', width: 120 }, - ], - rows: [ - { - id: 'anthropic', - cells: { - company: { - type: 'entity', - name: 'Anthropic', - domain: 'anthropic.com', - }, - url: { type: 'link', value: 'anthropic.com' }, - createdBy: { - type: 'person', - name: 'Dario Amodei', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei, - }, - address: { type: 'text', value: '18 Rue De Navarin' }, - accountOwner: { - type: 'person', - name: 'Dario Amodei', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei, - }, - icp: { type: 'boolean', value: true }, - arr: { type: 'number', value: '$500,000' }, - linkedin: { type: 'link', value: 'anthropic' }, - industry: { type: 'tag', value: 'AI Research' }, - mainContact: { - type: 'person', - name: 'Dario Amodei', - shortLabel: 'D', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei, - }, - employees: { type: 'number', value: '612' }, - opportunities: { - type: 'relation', - items: [ - { - name: 'Enterprise Expansion', - shortLabel: 'E', - tone: 'blue', - }, - ], - }, - added: { type: 'text', value: 'Jul 1, 2023' }, - }, - }, - { - id: 'linkedin', - cells: { - company: { - type: 'entity', - name: 'Linkedin', - domain: 'linkedin.com', - }, - url: { type: 'link', value: 'linkedin.com' }, - createdBy: { - type: 'person', - name: 'Reid Hoffman', - tone: 'purple', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.reidHoffman, - }, - address: { type: 'text', value: '1226 Moises Causeway' }, - accountOwner: { - type: 'person', - name: 'Ryan Roslansky', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.ryanRoslansky, - }, - icp: { type: 'boolean', value: false }, - arr: { type: 'number', value: '$1,000,000' }, - linkedin: { type: 'link', value: 'linkedin' }, - industry: { type: 'tag', value: 'Professional Networking' }, - mainContact: { - type: 'person', - name: 'Ryan Roslansky', - shortLabel: 'R', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.ryanRoslansky, - }, - employees: { type: 'number', value: '19,300' }, - opportunities: { - type: 'relation', - items: [ - { - name: 'Talent Outreach', - shortLabel: 'T', - tone: 'purple', - }, - ], - }, - added: { type: 'text', value: 'Jul 3, 2023' }, - }, - }, - { - id: 'slack', - cells: { - company: { type: 'entity', name: 'Slack', domain: 'slack.com' }, - url: { type: 'link', value: 'slack.com' }, - createdBy: { - type: 'person', - name: 'Stewart Butterfield', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.stewartButterfield, - }, - address: { type: 'text', value: '1316 Dameon Mountain' }, - accountOwner: { - type: 'person', - name: 'Stewart Butterfield', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.stewartButterfield, - }, - icp: { type: 'boolean', value: true }, - arr: { type: 'number', value: '$2,300,000' }, - linkedin: { type: 'link', value: 'slack' }, - industry: { type: 'tag', value: 'Collaboration Software' }, - mainContact: { - type: 'person', - name: 'Lidiane Jones', - shortLabel: 'LJ', - tone: 'pink', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.anonymousIndira, - }, - employees: { type: 'number', value: '4,500' }, - opportunities: { - type: 'relation', - items: [ - { - name: 'Workspace Renewal', - shortLabel: 'W', - tone: 'teal', - }, - ], - }, - added: { type: 'text', value: 'Jul 5, 2023' }, - }, - }, - { - id: 'notion', - cells: { - company: { - type: 'entity', - name: 'Notion', - domain: 'notion.com', - }, - url: { type: 'link', value: 'notion.com' }, - createdBy: { - type: 'person', - name: 'API - Key name', - tone: 'gray', - kind: 'api', - shortLabel: 'API', - }, - address: { type: 'text', value: '1162 Sammy Creek' }, - accountOwner: { - type: 'person', - name: 'Ivan Zhao', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao, - }, - icp: { type: 'boolean', value: false }, - arr: { type: 'number', value: '$750,000' }, - linkedin: { type: 'link', value: 'notion' }, - industry: { type: 'tag', value: 'Productivity Software' }, - mainContact: { - type: 'person', - name: 'Ivan Zhao', - shortLabel: 'I', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao, - }, - employees: { type: 'number', value: '620' }, - opportunities: { - type: 'relation', - items: [ - { - name: 'Workspace Consolidation', - shortLabel: 'W', - tone: 'gray', - }, - ], - }, - added: { type: 'text', value: 'Jul 8, 2023' }, - }, - }, - { - id: 'figma', - cells: { - company: { type: 'entity', name: 'Figma', domain: 'figma.com' }, - url: { type: 'link', value: 'figma.com' }, - createdBy: { - type: 'person', - name: 'Workflow name', - tone: 'gray', - kind: 'workflow', - shortLabel: 'WF', - }, - address: { type: 'text', value: '110 Oswald Junction' }, - accountOwner: { - type: 'person', - name: 'Dylan Field', - tone: 'purple', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.dylanField, - }, - icp: { type: 'boolean', value: true }, - arr: { type: 'number', value: '$3,500,000' }, - linkedin: { type: 'link', value: 'figma' }, - industry: { type: 'tag', value: 'Design Tools' }, - mainContact: { - type: 'person', - name: 'Dylan Field', - shortLabel: 'D', - tone: 'purple', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.dylanField, - }, - employees: { type: 'number', value: '1,300' }, - opportunities: { - type: 'relation', - items: [ - { - name: 'AI Prototyping', - shortLabel: 'AI', - tone: 'purple', - }, - { name: 'Design Ops', shortLabel: 'D', tone: 'teal' }, - ], - }, - added: { type: 'text', value: 'Jul 12, 2023' }, - }, - }, - { - id: 'github', - cells: { - company: { - type: 'entity', - name: 'Github', - domain: 'github.com', - }, - url: { type: 'link', value: 'github.com' }, - createdBy: { - type: 'person', - name: 'Chris Wanstrath', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.chrisWanstrath, - }, - address: { type: 'text', value: '3891 Ranchview Drive' }, - accountOwner: { - type: 'person', - name: 'Thomas Dohmke', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.thomasDohmke, - }, - icp: { type: 'boolean', value: true }, - arr: { type: 'number', value: '$900,000' }, - linkedin: { type: 'link', value: 'github' }, - industry: { type: 'tag', value: 'Developer Platform' }, - mainContact: { - type: 'person', - name: 'Thomas Dohmke', - shortLabel: 'T', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.thomasDohmke, - }, - employees: { type: 'number', value: '3,800' }, - opportunities: { - type: 'relation', - items: [ - { name: 'Copilot Rollout', shortLabel: 'C', tone: 'blue' }, - ], - }, - added: { type: 'text', value: 'Jul 14, 2023' }, - }, - }, - { - id: 'airbnb', - cells: { - company: { - type: 'entity', - name: 'Airbnb', - domain: 'airbnb.com', - }, - url: { type: 'link', value: 'airbnb.com' }, - createdBy: { - type: 'person', - name: 'Joe Gebbia', - tone: 'pink', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.joeGebbia, - }, - address: { type: 'text', value: '4517 Washington Avenue' }, - accountOwner: { - type: 'person', - name: 'Brian Chesky', - tone: 'pink', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.brianChesky, - }, - icp: { type: 'boolean', value: true }, - arr: { type: 'number', value: '$4,200,000' }, - linkedin: { type: 'link', value: 'airbnb' }, - industry: { type: 'tag', value: 'Travel' }, - mainContact: { - type: 'person', - name: 'Brian Chesky', - shortLabel: 'B', - tone: 'pink', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.brianChesky, - }, - employees: { type: 'number', value: '6,900' }, - opportunities: { - type: 'relation', - items: [{ name: 'Host Ops', shortLabel: 'H', tone: 'pink' }], - }, - added: { type: 'text', value: 'Jul 15, 2023' }, - }, - }, - { - id: 'stripe', - cells: { - company: { - type: 'entity', - name: 'Stripe', - domain: 'stripe.com', - }, - url: { type: 'link', value: 'stripe.com' }, - createdBy: { - type: 'person', - name: 'Patrick Collison', - tone: 'blue', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison, - }, - address: { type: 'text', value: '2118 Thornridge Circle' }, - accountOwner: { - type: 'person', - name: 'Patrick Collison', - tone: 'blue', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison, - }, - icp: { type: 'boolean', value: true }, - arr: { type: 'number', value: '$1,800,000' }, - linkedin: { type: 'link', value: 'stripe' }, - industry: { type: 'tag', value: 'Payments' }, - mainContact: { - type: 'person', - name: 'Patrick Collison', - shortLabel: 'P', - tone: 'blue', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison, - }, - employees: { type: 'number', value: '7,400' }, - opportunities: { - type: 'relation', - items: [ - { - name: 'Billing Expansion', - shortLabel: 'B', - tone: 'purple', - }, - ], - }, - added: { type: 'text', value: 'Jul 17, 2023' }, - }, - }, - { - id: 'sequoia', - cells: { - company: { - type: 'entity', - name: 'Sequoia', - domain: 'sequoia.com', - }, - url: { type: 'link', value: 'sequoia.com' }, - createdBy: { - type: 'person', - name: 'Roelof Botha', - tone: 'green', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.roelofBotha, - }, - address: { type: 'text', value: '1316 Dameon Mountain' }, - accountOwner: { - type: 'person', - name: 'Roelof Botha', - tone: 'green', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.roelofBotha, - }, - icp: { type: 'boolean', value: false }, - arr: { type: 'number', value: '$6,000,000' }, - linkedin: { type: 'link', value: 'sequoia' }, - industry: { type: 'tag', value: 'Venture Capital' }, - mainContact: { - type: 'person', - name: 'Roelof Botha', - shortLabel: 'R', - tone: 'green', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.roelofBotha, - }, - employees: { type: 'number', value: '1,100' }, - opportunities: { - type: 'relation', - items: [{ name: 'Fund Ops', shortLabel: 'F', tone: 'green' }], - }, - added: { type: 'text', value: 'Jul 20, 2023' }, - }, - }, - { - id: 'segment', - cells: { - company: { - type: 'entity', - name: 'Segment', - domain: 'segment.com', - }, - url: { type: 'link', value: 'segment.com' }, - createdBy: { - type: 'person', - name: 'Peter Reinhardt', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.peterReinhardt, - }, - address: { type: 'text', value: '8502 Preston Rd. East' }, - accountOwner: { - type: 'person', - name: 'Peter Reinhardt', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.peterReinhardt, - }, - icp: { type: 'boolean', value: true }, - arr: { type: 'number', value: '$2,750,000' }, - linkedin: { type: 'link', value: 'segment' }, - industry: { type: 'tag', value: 'Customer Data' }, - mainContact: { - type: 'person', - name: 'Peter Reinhardt', - shortLabel: 'P', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.peterReinhardt, - }, - employees: { type: 'number', value: '1,550' }, - opportunities: { - type: 'relation', - items: [ - { - name: 'Warehouse Rollout', - shortLabel: 'W', - tone: 'teal', - }, - ], - }, - added: { type: 'text', value: 'Jul 21, 2023' }, - }, - }, - { - id: 'mailchimp', - cells: { - company: { - type: 'entity', - name: 'Mailchimp', - domain: 'mailchimp.com', - }, - url: { type: 'link', value: 'mailchimp.com' }, - createdBy: { - type: 'person', - name: 'Ben Chestnut', - tone: 'amber', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.benChestnut, - }, - address: { type: 'text', value: '3517 W. Gray St.' }, - accountOwner: { - type: 'person', - name: 'Ben Chestnut', - tone: 'amber', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.benChestnut, - }, - icp: { type: 'boolean', value: false }, - arr: { type: 'number', value: '$1,250,000' }, - linkedin: { type: 'link', value: 'mailchimp' }, - industry: { type: 'tag', value: 'Marketing Automation' }, - mainContact: { - type: 'person', - name: 'Rania Succar', - shortLabel: 'R', - tone: 'amber', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.anonymousLaura, - }, - employees: { type: 'number', value: '1,900' }, - opportunities: { - type: 'relation', - items: [ - { - name: 'Lifecycle Campaigns', - shortLabel: 'L', - tone: 'amber', - }, - ], - }, - added: { type: 'text', value: 'Jul 23, 2023' }, - }, - }, - { - id: 'accel', - cells: { - company: { type: 'entity', name: 'Accel', domain: 'accel.com' }, - url: { type: 'link', value: 'accel.com' }, - createdBy: { - type: 'person', - name: 'Ray Damm', - tone: 'purple', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.rayDamm, - }, - address: { type: 'text', value: '4140 Parker Rd.' }, - accountOwner: { - type: 'person', - name: 'Ping Li', - tone: 'purple', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.pingLi, - }, - icp: { type: 'boolean', value: true }, - arr: { type: 'number', value: '$5,800,000' }, - linkedin: { type: 'link', value: 'accel' }, - industry: { type: 'tag', value: 'Venture Capital' }, - mainContact: { - type: 'person', - name: 'Ping Li', - shortLabel: 'P', - tone: 'purple', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.pingLi, - }, - employees: { type: 'number', value: '540' }, - opportunities: { - type: 'relation', - items: [ - { name: 'Portfolio Sync', shortLabel: 'P', tone: 'purple' }, - ], - }, - added: { type: 'text', value: 'Jul 24, 2023' }, - }, - }, - { - id: 'founders-fund', - cells: { - company: { - type: 'entity', - name: 'Founders Fund', - domain: 'foundersfund.com', - }, - url: { type: 'link', value: 'foundersfund.com' }, - createdBy: { - type: 'person', - name: 'System', - tone: 'gray', - kind: 'system', - shortLabel: 'SYS', - }, - address: { type: 'text', value: '2715 Ash Dr. San Jose' }, - accountOwner: { - type: 'person', - name: 'Peter Thiel', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.peterThiel, - }, - icp: { type: 'boolean', value: true }, - arr: { type: 'number', value: '$2,100,000' }, - linkedin: { type: 'link', value: 'foundersfund' }, - industry: { type: 'tag', value: 'Private Equity' }, - mainContact: { - type: 'person', - name: 'Peter Thiel', - shortLabel: 'P', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.peterThiel, - }, - employees: { type: 'number', value: '734' }, - opportunities: { - type: 'relation', - items: [ - { name: 'Fundraising', shortLabel: 'F', tone: 'gray' }, - ], - }, - added: { type: 'text', value: 'Jul 25, 2023' }, - }, - }, - { - id: 'google', - cells: { - company: { - type: 'entity', - name: 'Google', - domain: 'google.com', - }, - url: { type: 'link', value: 'google.com' }, - createdBy: { - type: 'person', - name: 'Sundar Pichai', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.sundarPichai, - }, - address: { type: 'text', value: '4140 Parker Rd.' }, - accountOwner: { - type: 'person', - name: 'Sundar Pichai', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.sundarPichai, - }, - icp: { type: 'boolean', value: false }, - arr: { type: 'number', value: '$7,500,000' }, - linkedin: { type: 'link', value: 'google' }, - industry: { type: 'tag', value: 'Computer Software' }, - mainContact: { - type: 'person', - name: 'Sundar Pichai', - shortLabel: 'S', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.sundarPichai, - }, - employees: { type: 'number', value: '734' }, - opportunities: { - type: 'relation', - items: [ - { - name: 'Google AI and Data Solutions', - shortLabel: 'G', - tone: 'teal', - }, - { name: 'Relation 2', shortLabel: 'L', tone: 'teal' }, - { name: 'Relation 3', shortLabel: 'L', tone: 'teal' }, - ], - }, - added: { type: 'text', value: 'Jul 1, 2023 2:25 pm' }, - }, - }, - ], - }), - }, - { - id: 'people', - label: 'People', - icon: { kind: 'tabler', name: 'user', tone: 'blue' }, - page: createTablePage({ - title: 'All People', - count: 5, - columns: [ - { id: 'name', label: 'Name', width: 180, isFirstColumn: true }, - { id: 'company', label: 'Company', width: 160 }, - { id: 'email', label: 'Email', width: 200 }, - { id: 'phone', label: 'Phone', width: 160 }, - { id: 'jobTitle', label: 'Job Title', width: 160 }, - { id: 'city', label: 'City', width: 120 }, - { id: 'linkedin', label: 'Linkedin', width: 140 }, - { id: 'added', label: 'Added', width: 160 }, - ], - rows: [ - { - id: 'dario-amodei', - cells: { - name: { - type: 'person', - name: 'Dario Amodei', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei, - }, - company: { - type: 'entity', - name: 'Anthropic', - domain: 'anthropic.com', - }, - email: { type: 'link', value: 'dario@anthropic.com' }, - phone: { type: 'text', value: '+1 415 555 0101' }, - jobTitle: { type: 'text', value: 'CEO' }, - city: { type: 'text', value: 'San Francisco' }, - linkedin: { type: 'link', value: 'dario-amodei' }, - added: { type: 'text', value: 'Jul 3, 2023' }, - }, - }, - { - id: 'ryan-roslansky', - cells: { - name: { - type: 'person', - name: 'Ryan Roslansky', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.ryanRoslansky, - }, - company: { - type: 'entity', - name: 'Linkedin', - domain: 'linkedin.com', - }, - email: { type: 'link', value: 'ryan@linkedin.com' }, - phone: { type: 'text', value: '+1 650 555 0134' }, - jobTitle: { type: 'text', value: 'CEO' }, - city: { type: 'text', value: 'Sunnyvale' }, - linkedin: { type: 'link', value: 'ryanroslansky' }, - added: { type: 'text', value: 'Jul 28, 2023' }, - }, - }, - { - id: 'stewart-butterfield', - cells: { - name: { - type: 'person', - name: 'Stewart Butterfield', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.stewartButterfield, - }, - company: { - type: 'entity', - name: 'Slack', - domain: 'slack.com', - }, - email: { type: 'link', value: 'stewart@slack.com' }, - phone: { type: 'text', value: '+1 415 555 0142' }, - jobTitle: { type: 'text', value: 'Co-founder' }, - city: { type: 'text', value: 'San Francisco' }, - linkedin: { type: 'link', value: 'stewart-butterfield' }, - added: { type: 'text', value: 'Jul 18, 2023' }, - }, - }, - { - id: 'ivan-zhao', - cells: { - name: { - type: 'person', - name: 'Ivan Zhao', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao, - }, - company: { - type: 'entity', - name: 'Notion', - domain: 'notion.com', - }, - email: { type: 'link', value: 'ivan@notion.com' }, - phone: { type: 'text', value: '+1 628 555 0186' }, - jobTitle: { type: 'text', value: 'CEO' }, - city: { type: 'text', value: 'San Francisco' }, - linkedin: { type: 'link', value: 'ivanhzhao' }, - added: { type: 'text', value: 'Jul 8, 2023' }, - }, - }, - { - id: 'dylan-field', - cells: { - name: { - type: 'person', - name: 'Dylan Field', - tone: 'purple', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.dylanField, - }, - company: { - type: 'entity', - name: 'Figma', - domain: 'figma.com', - }, - email: { type: 'link', value: 'dylan@figma.com' }, - phone: { type: 'text', value: '+1 415 555 0128' }, - jobTitle: { type: 'text', value: 'CEO' }, - city: { type: 'text', value: 'San Francisco' }, - linkedin: { type: 'link', value: 'dylanfield' }, - added: { type: 'text', value: 'Jul 12, 2023' }, - }, - }, - ], - }), - }, - { - id: 'opportunities', - label: 'Opportunities', - icon: { kind: 'tabler', name: 'targetArrow', tone: 'red' }, - page: OPPORTUNITY_KANBAN_PAGE, - }, - { - id: 'tasks', - label: 'Tasks', - icon: { kind: 'tabler', name: 'checkbox', tone: 'teal' }, - page: createTablePage({ - title: 'All Tasks', - count: 2, - columns: [ - { id: 'title', label: 'Title', width: 220, isFirstColumn: true }, - { id: 'assignee', label: 'Assignee', width: 160 }, - { id: 'dueDate', label: 'Due Date', width: 160 }, - { id: 'relatedTo', label: 'Related To', width: 160 }, - { id: 'status', label: 'Status', width: 140 }, - ], - rows: [ - { - id: 'send-nda', - cells: { - title: { - type: 'text', - value: 'Send NDA', - shortLabel: 'S', - tone: 'teal', - }, - assignee: { - type: 'person', - name: 'Dario Amodei', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei, - }, - dueDate: { type: 'text', value: 'Oct 25, 2023' }, - relatedTo: { - type: 'entity', - name: 'Anthropic', - domain: 'anthropic.com', - }, - status: { type: 'tag', value: 'To Do' }, - }, - }, - { - id: 'review-proposal', - cells: { - title: { - type: 'text', - value: 'Review proposal', - shortLabel: 'R', - tone: 'teal', - }, - assignee: { - type: 'person', - name: 'Stewart Butterfield', - tone: 'teal', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.stewartButterfield, - }, - dueDate: { type: 'text', value: 'Oct 28, 2023' }, - relatedTo: { - type: 'entity', - name: 'Slack', - domain: 'slack.com', - }, - status: { type: 'tag', value: 'In Progress' }, - }, - }, - ], - }), - }, - { - id: 'notes', - label: 'Notes', - icon: { kind: 'tabler', name: 'notes', tone: 'teal' }, - page: createTablePage({ - title: 'All Notes', - count: 2, - columns: [ - { id: 'title', label: 'Title', width: 240, isFirstColumn: true }, - { id: 'createdBy', label: 'Created By', width: 160 }, - { id: 'relatedTo', label: 'Related To', width: 160 }, - { id: 'added', label: 'Added', width: 180 }, - ], - rows: [ - { - id: 'discovery-call', - cells: { - title: { - type: 'text', - value: 'Discovery call notes', - shortLabel: 'D', - tone: 'green', - }, - createdBy: { - type: 'person', - name: 'Ivan Zhao', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao, - }, - relatedTo: { - type: 'entity', - name: 'Notion', - domain: 'notion.com', - }, - added: { type: 'text', value: 'Sep 2, 2023' }, - }, - }, - { - id: 'design-system-meeting', - cells: { - title: { - type: 'text', - value: 'Design system meeting', - shortLabel: 'D', - tone: 'green', - }, - createdBy: { - type: 'person', - name: 'Dylan Field', - tone: 'purple', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.dylanField, - }, - relatedTo: { - type: 'entity', - name: 'Figma', - domain: 'figma.com', - }, - added: { type: 'text', value: 'Oct 18, 2023' }, - }, - }, - ], - }), - }, - { - id: 'dashboards', - label: 'Dashboards', - icon: { kind: 'tabler', name: 'layoutDashboard', tone: 'gray' }, - page: createTablePage({ - title: 'All Dashboards', - count: 2, - columns: [ - { id: 'name', label: 'Name', width: 240, isFirstColumn: true }, - { id: 'createdBy', label: 'Created By', width: 160 }, - { id: 'added', label: 'Last Edited', width: 160 }, - ], - rows: [ - { - id: 'sales-dashboard', - cells: { - name: { - type: 'text', - value: 'Sales Dashboard', - shortLabel: 'S', - targetLabel: 'Sales Dashboard', - tone: 'amber', - }, - createdBy: { - type: 'person', - name: 'Dario Amodei', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei, - }, - added: { type: 'text', value: 'Oct 24, 2023' }, - }, - }, - { - id: 'pipeline-health', - cells: { - name: { - type: 'text', - value: 'Pipeline Health', - shortLabel: 'P', - tone: 'blue', - }, - createdBy: { - type: 'person', - name: 'Patrick Collison', - tone: 'blue', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison, - }, - added: { type: 'text', value: 'Oct 19, 2023' }, - }, - }, - ], - }), - }, - { - id: 'workflows', - label: 'Workflows', - icon: { kind: 'tabler', name: 'settingsAutomation', tone: 'orange' }, - showChevron: true, - items: [ - { - id: 'workflow-create-company-when-adding-a-new-person', - label: 'Create company when adding a new person', - icon: { - color: '#451E11', - kind: 'avatar', - label: 'C', - tone: 'orange', - shape: 'circle', - }, - page: { - type: 'workflow', - header: { - navbarActions: [ - { icon: 'chevronDown', variant: 'icon' }, - { icon: 'chevronUp', variant: 'icon' }, - { icon: 'heart', variant: 'icon' }, - { icon: 'playerPause', label: 'Deactivate' }, - { icon: 'repeat', label: 'See Runs' }, - { icon: 'plus', label: 'Add a Node' }, - { icon: 'dotsVertical', trailingLabel: '⌘K' }, - ], - title: 'Create company when adding a new person', - }, - }, - }, - { - id: 'workflow-send-email-sequence', - label: 'Send email sequence when deal is engaged', - icon: { - color: '#451E11', - kind: 'avatar', - label: 'S', - tone: 'orange', - shape: 'circle', - }, - page: { - type: 'workflow', - header: { - navbarActions: [ - { icon: 'chevronDown', variant: 'icon' }, - { icon: 'chevronUp', variant: 'icon' }, - { icon: 'heart', variant: 'icon' }, - { icon: 'playerPause', label: 'Deactivate' }, - { icon: 'repeat', label: 'See Runs' }, - { icon: 'plus', label: 'Add a Node' }, - { icon: 'dotsVertical', trailingLabel: '⌘K' }, - ], - title: 'Send email sequence when deal is engaged', - }, - nodes: [ - { - id: 'trigger', - x: 415, - y: 60, - width: 200, - label: 'Trigger', - title: 'Manual trigger', - iconName: 'plug', - }, - { - id: 'iterator', - x: 420, - y: 230, - width: 190, - label: 'Action', - title: 'Iterator', - iconName: 'repeat', - }, - { - id: 'send-email', - x: 590, - y: 340, - width: 200, - label: 'Action', - title: 'Send Email', - iconName: 'mail', - }, - ], - edges: [ - { from: 'trigger', to: 'iterator', type: 'vertical' }, - { - from: 'iterator', - to: 'send-email', - type: 'loopRight', - }, - { - from: 'send-email', - to: 'trigger', - type: 'loopBack', - }, - ], - branchLabels: [ - { x: 635, y: 248, text: 'loop' }, - { x: 482, y: 318, text: 'completed' }, - ], - plusNode: { x: 515, y: 380 }, - }, - }, - { - id: 'workflow-list', - label: 'All Workflows', - icon: { kind: 'tabler', name: 'settingsAutomation', tone: 'gray' }, - page: createTablePage({ - title: 'All Workflows', - count: 2, - columns: [ - { id: 'name', label: 'Name', width: 240, isFirstColumn: true }, - { id: 'status', label: 'Status', width: 140 }, - { id: 'lastRun', label: 'Last Run', width: 200 }, - ], - rows: [ - { - id: 'create-company-when-adding-a-new-person', - cells: { - name: { - type: 'text', - value: 'Create company when adding a new person', - shortLabel: 'C', - targetLabel: 'Create company when adding a new person', - tone: 'orange', - }, - status: { type: 'tag', value: 'Active' }, - lastRun: { type: 'text', value: 'Oct 24, 2023 10:00 am' }, - }, - }, - { - id: 'nurture', - cells: { - name: { - type: 'text', - value: 'Nurture Sequence', - shortLabel: 'N', - tone: 'amber', - }, - status: { type: 'tag', value: 'Inactive' }, - lastRun: { type: 'text', value: 'Oct 20, 2023 3:15 pm' }, - }, - }, - ], - }), - }, - { - id: 'workflow-runs', - label: 'Workflows runs', - icon: { kind: 'tabler', name: 'playerPlay', tone: 'gray' }, - page: createTablePage({ - title: 'All Runs', - count: 2, - columns: [ - { - id: 'runId', - label: 'Run ID', - width: 160, - isFirstColumn: true, - }, - { id: 'workflow', label: 'Workflow', width: 200 }, - { id: 'status', label: 'Status', width: 120 }, - { id: 'startedAt', label: 'Started At', width: 200 }, - { id: 'duration', label: 'Duration', width: 120 }, - ], - rows: [ - { - id: 'run-12345', - cells: { - runId: { - type: 'text', - value: 'run_12345', - shortLabel: 'R', - tone: 'amber', - }, - workflow: { type: 'text', value: 'New Lead Assignment' }, - status: { type: 'tag', value: 'Success' }, - startedAt: { type: 'text', value: 'Oct 24, 2023 10:00 am' }, - duration: { type: 'text', value: '2s' }, - }, - }, - { - id: 'run-12346', - cells: { - runId: { - type: 'text', - value: 'run_12346', - shortLabel: 'R', - tone: 'amber', - }, - workflow: { type: 'text', value: 'Nurture Sequence' }, - status: { type: 'tag', value: 'Failed' }, - startedAt: { type: 'text', value: 'Oct 20, 2023 3:15 pm' }, - duration: { type: 'text', value: '5s' }, - }, - }, - ], - }), - }, - { - id: 'workflow-versions', - label: 'Workflows versions', - icon: { kind: 'tabler', name: 'versions', tone: 'gray' }, - page: createTablePage({ - title: 'All Versions', - count: 2, - columns: [ - { - id: 'version', - label: 'Version', - width: 120, - isFirstColumn: true, - }, - { id: 'workflow', label: 'Workflow', width: 200 }, - { id: 'publishedAt', label: 'Published At', width: 200 }, - { id: 'publishedBy', label: 'Published By', width: 160 }, - ], - rows: [ - { - id: 'v2-lead', - cells: { - version: { - type: 'text', - value: 'v2', - shortLabel: 'V', - tone: 'amber', - }, - workflow: { type: 'text', value: 'New Lead Assignment' }, - publishedAt: { - type: 'text', - value: 'Oct 15, 2023 9:00 am', - }, - publishedBy: { - type: 'person', - name: 'Ivan Zhao', - shortLabel: 'I', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao, - }, - }, - }, - { - id: 'v1-lead', - cells: { - version: { - type: 'text', - value: 'v1', - shortLabel: 'V', - tone: 'amber', - }, - workflow: { type: 'text', value: 'New Lead Assignment' }, - publishedAt: { - type: 'text', - value: 'Sep 10, 2023 1:00 pm', - }, - publishedBy: { - type: 'person', - name: 'Ivan Zhao', - shortLabel: 'I', - tone: 'gray', - kind: 'person', - avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao, - }, - }, - }, - ], - }), - }, - ], - }, - { - id: 'book-demo', - label: 'Book a demo', - href: 'https://cal.com/forms/f7841033-0a20-4958-8c92-4e34ec128a81', - icon: { - kind: 'brand', - brand: 'twenty', - imageSrc: '/images/home/hero/twenty-demo-logo.webp', - overlay: 'link', - }, - }, - ], - }, -}; diff --git a/packages/twenty-website/src/app/[locale]/product/_components/ProductDemo.tsx b/packages/twenty-website/src/app/[locale]/product/_components/ProductDemo.tsx index 279aea97e4..8512f467d2 100644 --- a/packages/twenty-website/src/app/[locale]/product/_components/ProductDemo.tsx +++ b/packages/twenty-website/src/app/[locale]/product/_components/ProductDemo.tsx @@ -1,4 +1,3 @@ -import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data'; import { Container, Eyebrow, @@ -7,7 +6,7 @@ import { LinkButton, } from '@/design-system/components'; import { getServerI18n } from '@/lib/i18n/server'; -import { AppPreview } from '@/sections/AppPreview'; +import { APP_PREVIEW_DATA, AppPreview } from '@/sections/AppPreview'; import { theme } from '@/theme'; import { css } from '@linaria/core'; import { styled } from '@linaria/react'; @@ -17,7 +16,9 @@ import NextImage from 'next/image'; const RootWrapper = styled.div` display: grid; - grid-template-columns: 1fr; + grid-template-columns: minmax(0, 1fr); + min-width: 0; + overflow: clip; position: relative; width: 100%; `; @@ -57,8 +58,9 @@ const StyledSection = styled.section` const StyledContainer = styled(Container)` display: grid; - grid-template-columns: 1fr; + grid-template-columns: minmax(0, 1fr); justify-items: center; + min-width: 0; padding-bottom: ${theme.spacing(16)}; padding-left: ${theme.spacing(4)}; padding-right: ${theme.spacing(4)}; @@ -83,7 +85,6 @@ const CtasContainer = styled.div` `; const PreviewRoot = styled.div` - margin-bottom: ${theme.spacing(11)}; margin-top: ${theme.spacing(12)}; width: 100%; diff --git a/packages/twenty-website/src/app/[locale]/product/_components/ProductHero.tsx b/packages/twenty-website/src/app/[locale]/product/_components/ProductHero.tsx index a7305ec691..395e18151a 100644 --- a/packages/twenty-website/src/app/[locale]/product/_components/ProductHero.tsx +++ b/packages/twenty-website/src/app/[locale]/product/_components/ProductHero.tsx @@ -1,10 +1,11 @@ import { msg } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; -import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data'; import { AI_HERO_TABS } from '@/app/[locale]/product/ai-hero-tabs.data'; +import { APP_PREVIEW_DATA } from '@/sections/AppPreview'; import { Heading, HeadingPart } from '@/design-system/components'; import { getServerI18n } from '@/lib/i18n/server'; +import { TalkToUsButton } from '@/sections/ContactCal'; import { HeroVisualScroll } from '@/sections/Hero'; export function ProductHero() { @@ -13,12 +14,14 @@ export function ProductHero() { return ( - AI that actually + + ...with AI that actually +
helps you work faster @@ -28,7 +31,7 @@ export function ProductHero() { ctaHref="https://app.twenty.com/welcome" ctaLabel={i18n._(msg`Get started`)} introBody={i18n._( - msg`Track relationships, manage pipelines, and take action quickly with a CRM that feels intuitive from day one.`, + msg`Track relationships, manage pipelines, and take action quickly with an intuitive CRM that helps your team move faster from day one with confidence.`, )} introHeading={ @@ -40,6 +43,13 @@ export function ProductHero() {
} + introSecondaryCta={ + + } tabs={AI_HERO_TABS} visual={APP_PREVIEW_DATA.visual} /> diff --git a/packages/twenty-website/src/app/[locale]/product/_components/feature/ProductFeature.tsx b/packages/twenty-website/src/app/[locale]/product/_components/feature/ProductFeature.tsx index 4352a4009d..8cdf1637ac 100644 --- a/packages/twenty-website/src/app/[locale]/product/_components/feature/ProductFeature.tsx +++ b/packages/twenty-website/src/app/[locale]/product/_components/feature/ProductFeature.tsx @@ -4,6 +4,7 @@ import { Heading, HeadingPart, } from '@/design-system/components'; +import { type InformativeIconKey } from '@/icons'; import { type Page, Pages } from '@/lib/pages'; import { theme, type Scheme } from '@/theme'; import { Trans } from '@lingui/react/macro'; @@ -17,7 +18,7 @@ import { TileContent } from './TileContent'; import { TileVisual } from './TileVisual'; export type FeatureBullet = { - icon: string; + icon: InformativeIconKey; text: MessageDescriptor; }; diff --git a/packages/twenty-website/src/app/[locale]/product/_components/feature/TileContent.tsx b/packages/twenty-website/src/app/[locale]/product/_components/feature/TileContent.tsx index 500fe82984..4c99d38334 100644 --- a/packages/twenty-website/src/app/[locale]/product/_components/feature/TileContent.tsx +++ b/packages/twenty-website/src/app/[locale]/product/_components/feature/TileContent.tsx @@ -144,13 +144,11 @@ export function TileContent({ counter, spotlight, tile }: TileContentProps) { return ( - {Icon ? ( - - ) : null} + - {Icon ? ( - - ) : null} + {step.heading} diff --git a/packages/twenty-website/src/app/[locale]/product/_components/product-stepper/Visual.tsx b/packages/twenty-website/src/app/[locale]/product/_components/product-stepper/Visual.tsx index f60fb003e5..9d54763291 100644 --- a/packages/twenty-website/src/app/[locale]/product/_components/product-stepper/Visual.tsx +++ b/packages/twenty-website/src/app/[locale]/product/_components/product-stepper/Visual.tsx @@ -71,7 +71,7 @@ export function Visual({ activeStepIndex, stepVisuals, }: ProductStepperVisualProps) { - if (!stepVisuals || stepVisuals.length === 0) { + if (stepVisuals.length === 0) { return null; } diff --git a/packages/twenty-website/src/app/[locale]/product/_components/product-stepper/types.ts b/packages/twenty-website/src/app/[locale]/product/_components/product-stepper/types.ts index 11a670eb7c..52ca505ee1 100644 --- a/packages/twenty-website/src/app/[locale]/product/_components/product-stepper/types.ts +++ b/packages/twenty-website/src/app/[locale]/product/_components/product-stepper/types.ts @@ -1,3 +1,4 @@ +import type { InformativeIconKey } from '@/icons'; import type { MessageDescriptor } from '@lingui/core'; import type { ComponentType, ReactNode } from 'react'; @@ -8,6 +9,6 @@ export type StepperVisualProps = { export type ProductStepperStepType = { body: MessageDescriptor; heading: ReactNode; - icon: string; + icon: InformativeIconKey; visual: ComponentType; }; diff --git a/packages/twenty-website/src/app/[locale]/product/ai-hero-tabs.data.ts b/packages/twenty-website/src/app/[locale]/product/ai-hero-tabs.data.ts index 9e4da720ae..69412de842 100644 --- a/packages/twenty-website/src/app/[locale]/product/ai-hero-tabs.data.ts +++ b/packages/twenty-website/src/app/[locale]/product/ai-hero-tabs.data.ts @@ -4,19 +4,19 @@ import type { TabType } from '@/sections/Tabs'; export const AI_HERO_TABS: TabType[] = [ { - body: msg`Show me all deals closing this month`, - icon: 'search', + body: msg`Build a pipeline board grouped by stage`, + icon: 'kanban', }, { - body: msg`Create follow-up tasks for my top 10 accounts`, - icon: 'check', + body: msg`Generate tasks for my top 10 accounts`, + icon: 'checklist', }, { - body: msg`Summarize this customer's history`, - icon: 'edit', + body: msg`Build a dashboard of pipeline by stage`, + icon: 'chart', }, { - body: msg`Create a workflow that sends an email sequence`, - icon: 'code', + body: msg`Draft a workflow for an email sequence`, + icon: 'workflow', }, ]; diff --git a/packages/twenty-website/src/app/[locale]/product/page.tsx b/packages/twenty-website/src/app/[locale]/product/page.tsx index 55fa18de3f..8d6027b20e 100644 --- a/packages/twenty-website/src/app/[locale]/product/page.tsx +++ b/packages/twenty-website/src/app/[locale]/product/page.tsx @@ -10,11 +10,11 @@ import { ProductDemo } from '@/app/[locale]/product/_components/ProductDemo'; import { ProductFeature } from '@/app/[locale]/product/_components/feature'; import { Faq } from '@/sections/Faq'; import { ProductHero } from '@/app/[locale]/product/_components/ProductHero'; -import { Menu, MENU_DATA } from '@/sections/Menu'; +import { ProductHeroMenuSync } from '@/sections/Hero/components/ProductVisual/product-hero-menu-sync'; +import { MENU_DATA } from '@/sections/Menu'; import { ProductStepperSection } from '@/app/[locale]/product/_components/product-stepper'; import { ProductThreeCards } from '@/app/[locale]/product/_components/ProductThreeCards'; import { TrustedBy } from '@/sections/TrustedBy'; -import { theme } from '@/theme'; import type { AppLocale } from 'twenty-shared/translations'; export const generateMetadata = buildRouteMetadata('product'); @@ -41,12 +41,9 @@ export default async function ProductPage({ params }: ProductPageProps) { i18n.locale as AppLocale, )} /> - - - + + + diff --git a/packages/twenty-website/src/icons/clients/index.ts b/packages/twenty-website/src/icons/clients/index.ts index bf4434cdba..3b30d21fe1 100644 --- a/packages/twenty-website/src/icons/clients/index.ts +++ b/packages/twenty-website/src/icons/clients/index.ts @@ -15,11 +15,13 @@ export { NineDotsIcon } from './NineDots'; export { W3villaIcon } from './W3Villa'; export type { ClientIconProps } from './client-icon-props'; -export const CLIENT_ICONS: Record> = { +export const CLIENT_ICONS = { 'act-education': ActEducationIcon, 'alternative-partners': AlternativePartnersIcon, 'elevate-consulting': ElevateConsultingIcon, netzero: NetZeroIcon, 'nine-dots': NineDotsIcon, w3villa: W3villaIcon, -}; +} satisfies Record>; + +export type ClientIconKey = keyof typeof CLIENT_ICONS; diff --git a/packages/twenty-website/src/icons/index.ts b/packages/twenty-website/src/icons/index.ts index 9ea58d215d..1dc7e241e2 100644 --- a/packages/twenty-website/src/icons/index.ts +++ b/packages/twenty-website/src/icons/index.ts @@ -6,6 +6,7 @@ export { NetZeroIcon, NineDotsIcon, W3villaIcon, + type ClientIconKey, type ClientIconProps, } from './clients'; export { ArrowLeftIcon } from './functional/ArrowLeft'; @@ -27,6 +28,7 @@ export { INFORMATIVE_ICONS, SearchIcon, UsersIcon, + type InformativeIconKey, type InformativeIconProps, } from './informative'; export { Logo } from './logo/Logo'; @@ -36,5 +38,6 @@ export { LinkedInIcon, SOCIAL_ICONS, XIcon, + type SocialIconKey, type SocialIconProps, } from './social'; diff --git a/packages/twenty-website/src/icons/informative/Chart.tsx b/packages/twenty-website/src/icons/informative/Chart.tsx new file mode 100644 index 0000000000..4e93274228 --- /dev/null +++ b/packages/twenty-website/src/icons/informative/Chart.tsx @@ -0,0 +1,7 @@ +import { IconChartBar } from '@tabler/icons-react'; + +type ChartIconProps = { size: number; color: string; strokeWidth?: number }; + +export function ChartIcon({ size, color, strokeWidth = 2 }: ChartIconProps) { + return ; +} diff --git a/packages/twenty-website/src/icons/informative/Checklist.tsx b/packages/twenty-website/src/icons/informative/Checklist.tsx new file mode 100644 index 0000000000..52946638c6 --- /dev/null +++ b/packages/twenty-website/src/icons/informative/Checklist.tsx @@ -0,0 +1,11 @@ +import { IconChecklist } from '@tabler/icons-react'; + +type ChecklistIconProps = { size: number; color: string; strokeWidth?: number }; + +export function ChecklistIcon({ + size, + color, + strokeWidth = 2, +}: ChecklistIconProps) { + return ; +} diff --git a/packages/twenty-website/src/icons/informative/Edit.tsx b/packages/twenty-website/src/icons/informative/Edit.tsx index 65957cb75b..e3b00810b7 100644 --- a/packages/twenty-website/src/icons/informative/Edit.tsx +++ b/packages/twenty-website/src/icons/informative/Edit.tsx @@ -1,12 +1,6 @@ -'use client'; - -import { useId } from 'react'; - type EditIconProps = { size: number; color: string; strokeWidth?: number }; export function EditIcon({ size, color, strokeWidth = 2 }: EditIconProps) { - const clipPathId = useId().replace(/:/g, ''); - return ( - + - - - - - ); } diff --git a/packages/twenty-website/src/icons/informative/Kanban.tsx b/packages/twenty-website/src/icons/informative/Kanban.tsx new file mode 100644 index 0000000000..58f730e26e --- /dev/null +++ b/packages/twenty-website/src/icons/informative/Kanban.tsx @@ -0,0 +1,7 @@ +import { IconLayoutKanban } from '@tabler/icons-react'; + +type KanbanIconProps = { size: number; color: string; strokeWidth?: number }; + +export function KanbanIcon({ size, color, strokeWidth = 2 }: KanbanIconProps) { + return ; +} diff --git a/packages/twenty-website/src/icons/informative/Workflow.tsx b/packages/twenty-website/src/icons/informative/Workflow.tsx new file mode 100644 index 0000000000..87c7148826 --- /dev/null +++ b/packages/twenty-website/src/icons/informative/Workflow.tsx @@ -0,0 +1,11 @@ +import { IconHierarchy3 } from '@tabler/icons-react'; + +type WorkflowIconProps = { size: number; color: string; strokeWidth?: number }; + +export function WorkflowIcon({ + size, + color, + strokeWidth = 2, +}: WorkflowIconProps) { + return ; +} diff --git a/packages/twenty-website/src/icons/informative/index.ts b/packages/twenty-website/src/icons/informative/index.ts index 52bbab7bac..48b560e895 100644 --- a/packages/twenty-website/src/icons/informative/index.ts +++ b/packages/twenty-website/src/icons/informative/index.ts @@ -1,23 +1,31 @@ import { type ComponentType } from 'react'; import { BookIcon } from './Book'; +import { ChartIcon } from './Chart'; +import { ChecklistIcon } from './Checklist'; import { CheckIcon } from './Check'; import { CodeIcon } from './Code'; import { EditIcon } from './Edit'; import { EyeIcon } from './Eye'; +import { KanbanIcon } from './Kanban'; import { LightbulbIcon } from './Lightbulb'; import { SearchIcon } from './Search'; import { TagIcon } from './Tag'; import { UsersIcon } from './Users'; +import { WorkflowIcon } from './Workflow'; export { BookIcon } from './Book'; +export { ChartIcon } from './Chart'; +export { ChecklistIcon } from './Checklist'; export { CheckIcon } from './Check'; export { CodeIcon } from './Code'; export { EditIcon } from './Edit'; export { EyeIcon } from './Eye'; +export { KanbanIcon } from './Kanban'; export { LightbulbIcon } from './Lightbulb'; export { SearchIcon } from './Search'; export { TagIcon } from './Tag'; export { UsersIcon } from './Users'; +export { WorkflowIcon } from './Workflow'; export type InformativeIconProps = { size: number; @@ -25,17 +33,20 @@ export type InformativeIconProps = { strokeWidth?: number; }; -export const INFORMATIVE_ICONS: Record< - string, - ComponentType -> = { +export const INFORMATIVE_ICONS = { book: BookIcon, + chart: ChartIcon, check: CheckIcon, + checklist: ChecklistIcon, code: CodeIcon, edit: EditIcon, eye: EyeIcon, + kanban: KanbanIcon, lightbulb: LightbulbIcon, search: SearchIcon, tag: TagIcon, users: UsersIcon, -}; + workflow: WorkflowIcon, +} satisfies Record>; + +export type InformativeIconKey = keyof typeof INFORMATIVE_ICONS; diff --git a/packages/twenty-website/src/icons/social/index.ts b/packages/twenty-website/src/icons/social/index.ts index 634f47f7c0..6ee489ab30 100644 --- a/packages/twenty-website/src/icons/social/index.ts +++ b/packages/twenty-website/src/icons/social/index.ts @@ -11,9 +11,11 @@ export { XIcon } from './X'; export type SocialIconProps = { size: number; fillColor: string }; -export const SOCIAL_ICONS: Record> = { +export const SOCIAL_ICONS = { discord: DiscordIcon, github: GitHubIcon, linkedin: LinkedInIcon, x: XIcon, -}; +} satisfies Record>; + +export type SocialIconKey = keyof typeof SOCIAL_ICONS; diff --git a/packages/twenty-website/src/lib/customers/types.ts b/packages/twenty-website/src/lib/customers/types.ts index 52ff42b490..290689c787 100644 --- a/packages/twenty-website/src/lib/customers/types.ts +++ b/packages/twenty-website/src/lib/customers/types.ts @@ -1,3 +1,4 @@ +import type { ClientIconKey } from '@/icons'; import type { MessageDescriptor } from '@lingui/core'; import type { ReactNode } from 'react'; @@ -35,7 +36,7 @@ export type CaseStudyData = { title: ReactNode; author: string; authorAvatarSrc?: string; - clientIcon: string; + clientIcon: ClientIconKey; heroImageSrc: string; industry?: MessageDescriptor; authorRole?: MessageDescriptor; @@ -62,7 +63,7 @@ export type CaseStudyCatalogEntry = { title: MessageDescriptor; author: string; authorAvatarSrc?: string; - clientIcon: string; + clientIcon: ClientIconKey; heroImageSrc: string; }; catalogCard: CaseStudyData['catalogCard']; diff --git a/packages/twenty-website/src/sections/AppPreview/AppPreview.tsx b/packages/twenty-website/src/sections/AppPreview/AppPreview.tsx index b96b1e1c0d..a7ae1d3d06 100644 --- a/packages/twenty-website/src/sections/AppPreview/AppPreview.tsx +++ b/packages/twenty-website/src/sections/AppPreview/AppPreview.tsx @@ -4,16 +4,16 @@ import { scheduleVisualMount } from '@/lib/visual-runtime'; import { theme } from '@/theme'; import { styled } from '@linaria/react'; import { useEffect } from 'react'; -import type { AppPreviewConfig } from './types'; -import { AppWindow } from './AppWindow/AppWindow'; -import { Terminal } from './Terminal/Terminal'; -import { COLORS } from './Shared/utils/app-preview-theme'; -import { AppPreviewNavbar } from './Shell/AppPreviewNavbar'; -import { AppPreviewSidebar } from './Shell/AppPreviewSidebar'; -import { AppPreviewViewbar } from './Shell/AppPreviewViewbar'; -import { renderPageDefinition } from './Shell/PageRenderers'; +import { + AppPreviewFrame, + type AppPreviewFrameMode, +} from './AppWindow/AppPreviewFrame'; +import { AppPreviewLayout } from './Shell/AppPreviewLayout'; +import { type DesktopSidebarMode } from './Shell/AppPreviewSidebar'; import { preloadDeferredPages } from './Shell/preload-deferred-pages'; -import { useAppPreviewState } from './Shell/use-app-preview-state'; +import { useAppPreviewExperience } from './Shell/use-app-preview-experience'; +import { Terminal } from './Terminal/Terminal'; +import type { AppPreviewConfig } from './types'; import { WindowOrderProvider } from './WindowOrder/WindowOrderProvider'; const StyledAppPreview = styled.div` @@ -28,83 +28,47 @@ const StyledAppPreview = styled.div` } `; +const WINDOW_WIDTH = 1040; +const WINDOW_HEIGHT = 676; + const ShellScene = styled.div` - aspect-ratio: 1 / 1; - margin: 0 auto; - max-height: 740px; + height: ${WINDOW_HEIGHT}px; + margin: 0 auto ${theme.spacing(16)}; position: relative; - width: 100%; - - @media (min-width: ${theme.breakpoints.md}px) { - aspect-ratio: 1280 / 832; - } -`; - -const AppLayout = styled.div` - display: flex; - flex: 1 1 auto; - height: 100%; - min-width: 0; - overflow: hidden; - min-height: 0; - position: relative; - width: 100%; - z-index: 1; -`; - -const RightPane = styled.div` - display: flex; - flex: 1 1 0; - flex-direction: column; - gap: 12px; - min-height: 0; - min-width: 0; - padding: 12px 8px 12px 0; - - @media (min-width: ${theme.breakpoints.md}px) { - padding-right: 12px; - } -`; - -const IndexSurface = styled.div` - background: ${COLORS.background}; - border: 1px solid ${COLORS.border}; - border-radius: 8px; - display: flex; - flex: 1 1 auto; - flex-direction: column; - min-height: 0; - min-width: 0; - overflow: hidden; + width: ${WINDOW_WIDTH}px; `; type AppPreviewProps = { + desktopSidebarMode?: DesktopSidebarMode; + frameMode?: AppPreviewFrameMode; showTerminal?: boolean; visual: AppPreviewConfig; }; -export function AppPreview({ showTerminal = true, visual }: AppPreviewProps) { +export function AppPreview({ + desktopSidebarMode = 'expanded', + frameMode, + showTerminal = true, + visual, +}: AppPreviewProps) { const { activeItem, - activeLabel, + activeItemId, + activeItemLabel, activePage, + favorites, handleChatReset, handleJumpToConversationEnd, handleObjectCreated, - handleSelectLabel, - handleToggleFolder, + selectPageItem, + toggleFolder, highlightedItemId, openFolderIds, revealedObjectIds, - workspaceNav, - } = useAppPreviewState(visual); - const activeHeader = activePage?.header; - const showViewBar = - activePage !== null && - activePage !== undefined && - activePage.type !== 'dashboard' && - activePage.type !== 'record' && - activePage.type !== 'workflow'; + workspaceEntries, + } = useAppPreviewExperience(visual); + const resolvedFrameMode = frameMode ?? (showTerminal ? 'windowed' : 'static'); + const needsWindowOrder = showTerminal || resolvedFrameMode === 'windowed'; useEffect( () => @@ -117,61 +81,46 @@ export function AppPreview({ showTerminal = true, visual }: AppPreviewProps) { [], ); + const previewFrame = ( + + + + ); + + const previewContent = ( + <> + {previewFrame} + {showTerminal ? ( + + ) : null} + + ); + return ( - - - - - - - - - - {showViewBar ? ( - - ) : null} - - {activePage - ? renderPageDefinition( - activePage, - handleSelectLabel, - activeItem?.id ?? activeLabel, - ) - : null} - - - - - {showTerminal ? ( - - ) : null} - + {needsWindowOrder ? ( + {previewContent} + ) : ( + previewContent + )} ); diff --git a/packages/twenty-website/src/sections/AppPreview/AppWindow/AppPreviewFrame.tsx b/packages/twenty-website/src/sections/AppPreview/AppWindow/AppPreviewFrame.tsx new file mode 100644 index 0000000000..dd2278fe56 --- /dev/null +++ b/packages/twenty-website/src/sections/AppPreview/AppWindow/AppPreviewFrame.tsx @@ -0,0 +1,88 @@ +'use client'; + +import { styled } from '@linaria/react'; +import type { ReactNode } from 'react'; +import { theme } from '@/theme'; + +import { VISUAL_TOKENS } from '../Shared/utils/app-preview-tokens'; +import { WINDOW_SHADOWS } from '../Shared/utils/window-shadows'; +import { AppWindow } from './AppWindow'; +import { MacWindowBar } from './MacWindowBar'; + +export type AppPreviewFrameMode = 'static' | 'windowed'; + +const APP_PREVIEW_FRAME_MAX_WIDTH = 1040; +const APP_PREVIEW_FRAME_ASPECT_RATIO = '1280 / 832'; + +const StaticFrameRoot = styled.div` + align-items: stretch; + display: flex; + height: 100%; + justify-content: center; + width: 100%; +`; + +// compact fills the (fixed-height, fluid-width) scene box without aspect-ratio, +// so the board reflows instead of the whole window scaling with width. +// floatingShadow swaps the resting halo for a downward-biased shadow (product +// hero only), so the soft shadow drops below instead of haloing above the title. +const StaticShell = styled.div<{ $compact: boolean; $floatingShadow: boolean }>` + aspect-ratio: ${({ $compact }) => + $compact ? 'auto' : APP_PREVIEW_FRAME_ASPECT_RATIO}; + background-color: ${VISUAL_TOKENS.background.primary}; + background-image: ${VISUAL_TOKENS.background.noisy}; + border: 1px solid ${VISUAL_TOKENS.border.color.medium}; + border-radius: 20px; + box-shadow: ${({ $floatingShadow }) => + $floatingShadow + ? WINDOW_SHADOWS.mobileFloating + : WINDOW_SHADOWS.mobileResting}; + display: flex; + flex-direction: column; + height: ${({ $compact }) => ($compact ? '100%' : 'auto')}; + max-height: ${({ $compact }) => ($compact ? 'none' : '100%')}; + max-width: ${({ $compact }) => + $compact ? 'none' : `${APP_PREVIEW_FRAME_MAX_WIDTH}px`}; + overflow: hidden; + position: relative; + width: 100%; + + @media (min-width: ${theme.breakpoints.md}px) { + box-shadow: ${({ $floatingShadow }) => + $floatingShadow ? WINDOW_SHADOWS.floating : WINDOW_SHADOWS.resting}; + } +`; + +const StaticContent = styled.div` + display: flex; + flex: 1 1 auto; + min-height: 0; + width: 100%; +`; + +type AppPreviewFrameProps = { + children: ReactNode; + compact?: boolean; + floatingShadow?: boolean; + mode: AppPreviewFrameMode; +}; + +export function AppPreviewFrame({ + children, + compact = false, + floatingShadow = false, + mode, +}: AppPreviewFrameProps) { + if (mode === 'windowed') { + return {children}; + } + + return ( + + + + {children} + + + ); +} diff --git a/packages/twenty-website/src/sections/AppPreview/AppWindow/AppWindow.tsx b/packages/twenty-website/src/sections/AppPreview/AppWindow/AppWindow.tsx index 31141b6073..4cb4529948 100644 --- a/packages/twenty-website/src/sections/AppPreview/AppWindow/AppWindow.tsx +++ b/packages/twenty-website/src/sections/AppPreview/AppWindow/AppWindow.tsx @@ -18,8 +18,6 @@ const MIN_WIDTH = 640; const MIN_HEIGHT = 420; const MIN_EDGE_GAP = 0; const INITIAL_MAX_WIDTH = 1040; -const INITIAL_ASPECT_RATIO = 1280 / 832; -const MOBILE_PARENT_BREAKPOINT = 640; const Shell = styled.div<{ $isResizing: boolean; @@ -139,22 +137,8 @@ export const AppWindow = ({ children }: AppWindowProps) => { } const parentRect = parent.getBoundingClientRect(); - if (parentRect.width < MOBILE_PARENT_BREAKPOINT) { - const mobileWidth = parentRect.width; - const mobileHeight = Math.min( - parentRect.height, - mobileWidth / INITIAL_ASPECT_RATIO, - ); - setSize({ width: mobileWidth, height: mobileHeight }); - setPosition({ left: 0, top: 0 }); - return; - } - - const newWidth = Math.min(parentRect.width, INITIAL_MAX_WIDTH); - const newHeight = Math.min( - parentRect.height, - newWidth / INITIAL_ASPECT_RATIO, - ); + const newWidth = INITIAL_MAX_WIDTH; + const newHeight = parentRect.height; setSize({ width: newWidth, height: newHeight }); setPosition({ diff --git a/packages/twenty-website/src/sections/AppPreview/AppWindow/MacWindowBar.tsx b/packages/twenty-website/src/sections/AppPreview/AppWindow/MacWindowBar.tsx index 9d9708a8f9..32b6703585 100644 --- a/packages/twenty-website/src/sections/AppPreview/AppWindow/MacWindowBar.tsx +++ b/packages/twenty-website/src/sections/AppPreview/AppWindow/MacWindowBar.tsx @@ -5,9 +5,10 @@ import type { PointerEvent as ReactPointerEvent } from 'react'; import { TerminalTrafficLights } from '../Terminal/TerminalTrafficLights/TerminalTrafficLights'; type MacWindowBarProps = { + interactive?: boolean; + isDragging?: boolean; + onDragStart?: (event: ReactPointerEvent) => void; title?: string; - onDragStart: (event: ReactPointerEvent) => void; - isDragging: boolean; }; const BAR_BACKGROUND = '#F7F7F7'; @@ -17,12 +18,18 @@ const BAR_VERTICAL_PADDING = 8; const BAR_HORIZONTAL_PADDING = 12; const TRAFFIC_LIGHT_WIDTH = 52; -const BarRoot = styled.div<{ $isDragging: boolean }>` +const BarRoot = styled.div<{ $interactive: boolean; $isDragging: boolean }>` align-items: center; background: ${BAR_BACKGROUND}; border-bottom: 1px solid ${BAR_BORDER}; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65); - cursor: ${({ $isDragging }) => ($isDragging ? 'grabbing' : 'grab')}; + cursor: ${({ $interactive, $isDragging }) => { + if (!$interactive) { + return 'default'; + } + + return $isDragging ? 'grabbing' : 'grab'; + }}; display: grid; flex-shrink: 0; grid-template-columns: auto 1fr auto; @@ -47,12 +54,17 @@ const RightSpacer = styled.div` `; export const MacWindowBar = ({ - title = 'Twenty', + interactive = true, + isDragging = false, onDragStart, - isDragging, + title = 'Twenty', }: MacWindowBarProps) => { return ( - + {title} diff --git a/packages/twenty-website/src/sections/AppPreview/Data/normalize-page.ts b/packages/twenty-website/src/sections/AppPreview/Data/normalize-page.ts index f1b2ac4910..c5210c28b9 100644 --- a/packages/twenty-website/src/sections/AppPreview/Data/normalize-page.ts +++ b/packages/twenty-website/src/sections/AppPreview/Data/normalize-page.ts @@ -1,7 +1,7 @@ import type { KanbanPageDefinition, PageDefinition, - SidebarItemDef, + SidebarPageItemDef, TablePageDefinition, WorkflowPageDefinition, } from '../types'; @@ -80,12 +80,8 @@ function normalizeWorkflowPage( } export function normalizePage( - item: SidebarItemDef, + item: SidebarPageItemDef, defaults: PageDefaults, -): PageDefinition | null { - if (!item.page) { - return null; - } - +): PageDefinition { return normalizeExplicitPage(item.page, defaults); } diff --git a/packages/twenty-website/src/sections/AppPreview/Data/rocket-object.ts b/packages/twenty-website/src/sections/AppPreview/Data/rocket-object.ts index 3bcc885d3d..200f1235cc 100644 --- a/packages/twenty-website/src/sections/AppPreview/Data/rocket-object.ts +++ b/packages/twenty-website/src/sections/AppPreview/Data/rocket-object.ts @@ -32,7 +32,7 @@ const ROCKET_PAGE: TablePageDefinition = { }, serialNumber: { type: 'text', value: 'B1062' }, manufacturer: { type: 'entity', name: 'SpaceX', domain: 'spacex.com' }, - status: { type: 'tag', value: 'Active' }, + status: { type: 'select', color: 'green', value: 'Active' }, reusable: { type: 'boolean', value: true }, launchDate: { type: 'text', value: 'Apr 11, 2024' }, targetOrbit: { type: 'text', value: 'LEO' }, @@ -51,7 +51,7 @@ const ROCKET_PAGE: TablePageDefinition = { }, serialNumber: { type: 'text', value: 'S29' }, manufacturer: { type: 'entity', name: 'SpaceX', domain: 'spacex.com' }, - status: { type: 'tag', value: 'Testing' }, + status: { type: 'select', color: 'orange', value: 'Testing' }, reusable: { type: 'boolean', value: true }, launchDate: { type: 'text', value: 'Jun 6, 2024' }, targetOrbit: { type: 'text', value: 'Mars Transfer' }, @@ -74,7 +74,7 @@ const ROCKET_PAGE: TablePageDefinition = { name: 'Blue Origin', domain: 'blueorigin.com', }, - status: { type: 'tag', value: 'Active' }, + status: { type: 'select', color: 'green', value: 'Active' }, reusable: { type: 'boolean', value: true }, launchDate: { type: 'text', value: 'Jan 16, 2025' }, targetOrbit: { type: 'text', value: 'GTO' }, @@ -97,7 +97,7 @@ const ROCKET_PAGE: TablePageDefinition = { name: 'Rocket Lab', domain: 'rocketlabusa.com', }, - status: { type: 'tag', value: 'Active' }, + status: { type: 'select', color: 'green', value: 'Active' }, reusable: { type: 'boolean', value: false }, launchDate: { type: 'text', value: 'Sep 20, 2024' }, targetOrbit: { type: 'text', value: 'SSO' }, @@ -120,7 +120,7 @@ const ROCKET_PAGE: TablePageDefinition = { name: 'Arianespace', domain: 'arianespace.com', }, - status: { type: 'tag', value: 'Active' }, + status: { type: 'select', color: 'green', value: 'Active' }, reusable: { type: 'boolean', value: false }, launchDate: { type: 'text', value: 'Jul 9, 2024' }, targetOrbit: { type: 'text', value: 'GTO' }, @@ -143,7 +143,7 @@ const ROCKET_PAGE: TablePageDefinition = { name: 'ULA', domain: 'ulalaunch.com', }, - status: { type: 'tag', value: 'Active' }, + status: { type: 'select', color: 'green', value: 'Active' }, reusable: { type: 'boolean', value: false }, launchDate: { type: 'text', value: 'Oct 4, 2024' }, targetOrbit: { type: 'text', value: 'GEO' }, @@ -180,8 +180,8 @@ const LAUNCH_PAGE: TablePageDefinition = { cells: { name: { type: 'text', value: 'CRS-29', shortLabel: 'C', tone: 'blue' }, missionCode: { type: 'text', value: 'NASA-CRS-29' }, - status: { type: 'tag', value: 'Success' }, - missionType: { type: 'tag', value: 'Cargo' }, + status: { type: 'select', color: 'green', value: 'Success' }, + missionType: { type: 'select', color: 'orange', value: 'Cargo' }, plannedLaunchAt: { type: 'text', value: 'Nov 9, 2023' }, rocket: { type: 'relation', @@ -204,8 +204,8 @@ const LAUNCH_PAGE: TablePageDefinition = { tone: 'amber', }, missionCode: { type: 'text', value: 'NASA-ART-2' }, - status: { type: 'tag', value: 'Scheduled' }, - missionType: { type: 'tag', value: 'Crewed' }, + status: { type: 'select', color: 'blue', value: 'Scheduled' }, + missionType: { type: 'select', color: 'purple', value: 'Crewed' }, plannedLaunchAt: { type: 'text', value: 'Sep 26, 2025' }, rocket: { type: 'relation', @@ -228,8 +228,8 @@ const LAUNCH_PAGE: TablePageDefinition = { tone: 'amber', }, missionCode: { type: 'text', value: 'SPX-IFT-5' }, - status: { type: 'tag', value: 'Success' }, - missionType: { type: 'tag', value: 'Test' }, + status: { type: 'select', color: 'green', value: 'Success' }, + missionType: { type: 'select', color: 'purple', value: 'Test' }, plannedLaunchAt: { type: 'text', value: 'Oct 13, 2024' }, rocket: { type: 'relation', @@ -247,8 +247,8 @@ const LAUNCH_PAGE: TablePageDefinition = { cells: { name: { type: 'text', value: 'Euclid', shortLabel: 'E', tone: 'teal' }, missionCode: { type: 'text', value: 'ESA-EUC-1' }, - status: { type: 'tag', value: 'Success' }, - missionType: { type: 'tag', value: 'Commercial' }, + status: { type: 'select', color: 'green', value: 'Success' }, + missionType: { type: 'select', color: 'blue', value: 'Commercial' }, plannedLaunchAt: { type: 'text', value: 'Jul 1, 2023' }, rocket: { type: 'relation', @@ -271,8 +271,8 @@ const LAUNCH_PAGE: TablePageDefinition = { tone: 'purple', }, missionCode: { type: 'text', value: 'NASA-PSY-1' }, - status: { type: 'tag', value: 'Success' }, - missionType: { type: 'tag', value: 'Commercial' }, + status: { type: 'select', color: 'green', value: 'Success' }, + missionType: { type: 'select', color: 'blue', value: 'Commercial' }, plannedLaunchAt: { type: 'text', value: 'Oct 13, 2023' }, rocket: { type: 'relation', @@ -310,8 +310,8 @@ const PAYLOAD_PAGE: TablePageDefinition = { shortLabel: 'S', tone: 'blue', }, - payloadType: { type: 'tag', value: 'Satellite' }, - status: { type: 'tag', value: 'Deployed' }, + payloadType: { type: 'select', color: 'blue', value: 'Satellite' }, + status: { type: 'select', color: 'green', value: 'Deployed' }, customer: { type: 'relation', items: [{ name: 'Starlink', shortLabel: 'S', tone: 'blue' }], @@ -333,8 +333,8 @@ const PAYLOAD_PAGE: TablePageDefinition = { shortLabel: 'O', tone: 'amber', }, - payloadType: { type: 'tag', value: 'Crew Capsule' }, - status: { type: 'tag', value: 'Integrated' }, + payloadType: { type: 'select', color: 'purple', value: 'Crew Capsule' }, + status: { type: 'select', color: 'blue', value: 'Integrated' }, customer: { type: 'relation', items: [{ name: 'NASA', shortLabel: 'N', tone: 'red' }], @@ -356,8 +356,8 @@ const PAYLOAD_PAGE: TablePageDefinition = { shortLabel: 'D', tone: 'blue', }, - payloadType: { type: 'tag', value: 'Cargo' }, - status: { type: 'tag', value: 'Launched' }, + payloadType: { type: 'select', color: 'orange', value: 'Cargo' }, + status: { type: 'select', color: 'green', value: 'Launched' }, customer: { type: 'relation', items: [{ name: 'NASA', shortLabel: 'N', tone: 'red' }], @@ -379,8 +379,8 @@ const PAYLOAD_PAGE: TablePageDefinition = { shortLabel: 'P', tone: 'purple', }, - payloadType: { type: 'tag', value: 'Probe' }, - status: { type: 'tag', value: 'Deployed' }, + payloadType: { type: 'select', color: 'teal', value: 'Probe' }, + status: { type: 'select', color: 'green', value: 'Deployed' }, customer: { type: 'relation', items: [{ name: 'NASA', shortLabel: 'N', tone: 'red' }], @@ -402,8 +402,8 @@ const PAYLOAD_PAGE: TablePageDefinition = { shortLabel: 'E', tone: 'teal', }, - payloadType: { type: 'tag', value: 'Satellite' }, - status: { type: 'tag', value: 'Deployed' }, + payloadType: { type: 'select', color: 'blue', value: 'Satellite' }, + status: { type: 'select', color: 'green', value: 'Deployed' }, customer: { type: 'relation', items: [{ name: 'ESA', shortLabel: 'E', tone: 'teal' }], @@ -442,7 +442,7 @@ const LAUNCH_SITE_PAGE: TablePageDefinition = { siteCode: { type: 'text', value: 'KSC-39A' }, padName: { type: 'text', value: 'Launch Complex 39A' }, country: { type: 'text', value: 'United States' }, - siteStatus: { type: 'tag', value: 'Active' }, + siteStatus: { type: 'select', color: 'green', value: 'Active' }, }, }, { @@ -457,7 +457,7 @@ const LAUNCH_SITE_PAGE: TablePageDefinition = { siteCode: { type: 'text', value: 'CCSFS-40' }, padName: { type: 'text', value: 'Space Launch Complex 40' }, country: { type: 'text', value: 'United States' }, - siteStatus: { type: 'tag', value: 'Active' }, + siteStatus: { type: 'select', color: 'green', value: 'Active' }, }, }, { @@ -472,7 +472,7 @@ const LAUNCH_SITE_PAGE: TablePageDefinition = { siteCode: { type: 'text', value: 'SB-01' }, padName: { type: 'text', value: 'Orbital Launch Pad A' }, country: { type: 'text', value: 'United States' }, - siteStatus: { type: 'tag', value: 'Active' }, + siteStatus: { type: 'select', color: 'green', value: 'Active' }, }, }, { @@ -487,7 +487,7 @@ const LAUNCH_SITE_PAGE: TablePageDefinition = { siteCode: { type: 'text', value: 'VSFB-4E' }, padName: { type: 'text', value: 'Space Launch Complex 4E' }, country: { type: 'text', value: 'United States' }, - siteStatus: { type: 'tag', value: 'Active' }, + siteStatus: { type: 'select', color: 'green', value: 'Active' }, }, }, { @@ -502,7 +502,7 @@ const LAUNCH_SITE_PAGE: TablePageDefinition = { siteCode: { type: 'text', value: 'CSG-4' }, padName: { type: 'text', value: 'Ensemble de Lancement 4' }, country: { type: 'text', value: 'French Guiana' }, - siteStatus: { type: 'tag', value: 'Active' }, + siteStatus: { type: 'select', color: 'green', value: 'Active' }, }, }, ], diff --git a/packages/twenty-website/src/sections/AppPreview/Pages/Dashboard/DashboardCharts.tsx b/packages/twenty-website/src/sections/AppPreview/Pages/Dashboard/DashboardCharts.tsx new file mode 100644 index 0000000000..5d31b74f1a --- /dev/null +++ b/packages/twenty-website/src/sections/AppPreview/Pages/Dashboard/DashboardCharts.tsx @@ -0,0 +1,412 @@ +'use client'; + +import { useEffect, useId, useState, type CSSProperties } from 'react'; + +import { styled } from '@linaria/react'; + +import { createAnimationFrameLoop } from '@/lib/animation'; + +import type { + DashboardBarChart, + DashboardDonutChart, + DashboardLineChart, +} from '../../types'; +import { APP_FONT, COLORS } from '../../Shared/utils/app-preview-theme'; + +const ACCENT = '#8da4ef'; + +const ChartFrame = styled.div` + display: flex; + flex: 1; + min-height: 0; + width: 100%; +`; + +const AxisLabel = styled.span` + color: ${COLORS.textSecondary}; + font-family: ${APP_FONT}; + font-size: 10px; + line-height: 1; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const PlotArea = styled.div` + display: flex; + flex: 1; + min-height: 0; + position: relative; +`; + +const Gridlines = styled.div<{ $bottom: number }>` + bottom: ${({ $bottom }) => `${$bottom}px`}; + display: flex; + flex-direction: column; + justify-content: space-between; + left: 0; + pointer-events: none; + position: absolute; + right: 0; + top: 0; +`; + +const GridLine = styled.span` + border-top: 1px dashed ${COLORS.borderLight}; + display: block; + width: 100%; +`; + +const GRID_LINES = [0, 1, 2, 3]; + +// --- Line chart ------------------------------------------------------------- + +const LINE_W = 320; +const LINE_H = 120; +const LINE_PAD = 10; + +const LineSvg = styled.svg` + display: block; + height: 100%; + position: relative; + width: 100%; + z-index: 1; + + .line-stroke { + transition: opacity 420ms ease; + } + + .line-area { + transition: opacity 500ms ease 160ms; + } + + @media (prefers-reduced-motion: reduce) { + .line-area, + .line-stroke { + transition: none; + } + } +`; + +export function DashboardLineChart({ data }: { data: DashboardLineChart }) { + const [drawn, setDrawn] = useState(false); + const gradientId = `dashboard-line-fill-${useId().replace(/:/g, '')}`; + + useEffect(() => { + const task = createAnimationFrameLoop({ + onFrame: () => { + setDrawn(true); + return false; + }, + }); + + task.start(); + + return () => task.stop(); + }, []); + + const max = Math.max(...data.values); + const min = Math.min(...data.values); + const range = max - min || 1; + const stepX = (LINE_W - LINE_PAD * 2) / (data.values.length - 1); + + const points = data.values.map((value, index) => { + const x = LINE_PAD + index * stepX; + const y = LINE_PAD + (LINE_H - LINE_PAD * 2) * (1 - (value - min) / range); + + return [x, y] as const; + }); + + const linePath = points + .map( + ([x, y], index) => + `${index === 0 ? 'M' : 'L'}${x.toFixed(1)} ${y.toFixed(1)}`, + ) + .join(' '); + const lastX = points[points.length - 1][0]; + const firstX = points[0][0]; + const areaPath = `${linePath} L${lastX.toFixed(1)} ${LINE_H - LINE_PAD} L${firstX.toFixed(1)} ${LINE_H - LINE_PAD} Z`; + + return ( + + + + + + + ); +} + +// --- Bar chart -------------------------------------------------------------- + +const BarColumns = styled.div` + align-items: flex-end; + display: flex; + flex: 1; + gap: 10px; + min-height: 0; + padding-top: 8px; + position: relative; + width: 100%; + z-index: 1; +`; + +const BarColumn = styled.div` + align-items: center; + display: flex; + flex: 1; + flex-direction: column; + gap: 6px; + justify-content: flex-end; + min-width: 0; +`; + +const BarTrack = styled.div` + align-items: flex-end; + display: flex; + flex: 1; + min-height: 0; + width: 100%; +`; + +const Bar = styled.div<{ $heightPct: number; $index: number }>` + animation: barGrow 460ms cubic-bezier(0.22, 1, 0.36, 1) both; + animation-delay: ${({ $index }) => `${$index * 90}ms`}; + background: ${ACCENT}; + border-radius: 4px 4px 0 0; + height: ${({ $heightPct }) => `${$heightPct}%`}; + margin: 0 auto; + max-width: 28px; + transform-origin: bottom; + width: 100%; + + @keyframes barGrow { + from { + transform: scaleY(0); + } + to { + transform: scaleY(1); + } + } + + @media (prefers-reduced-motion: reduce) { + animation: none; + } +`; + +export function DashboardBarChart({ data }: { data: DashboardBarChart }) { + const max = Math.max(...data.bars.map((bar) => bar.value)) || 1; + + return ( + + + + + {data.bars.map((bar, index) => ( + + + + + {bar.label} + + ))} + + + + ); +} + +// --- Donut chart ------------------------------------------------------------ + +const DONUT_RADIUS = 15.915; +const DONUT_GAP = 2; + +const DonutWrap = styled.div` + align-items: center; + display: flex; + flex: 1; + gap: 12px; + min-height: 0; +`; + +const DonutFigure = styled.div` + flex-shrink: 0; + height: 96px; + position: relative; + width: 96px; +`; + +const DonutSvg = styled.svg` + display: block; + height: 100%; + transform: rotate(-90deg); + width: 100%; + + .donut-slice { + animation: donutSliceDraw 520ms cubic-bezier(0.22, 1, 0.36, 1) both; + } + + @keyframes donutSliceDraw { + from { + stroke-dasharray: 0 100; + } + to { + stroke-dasharray: var(--slice-pct) var(--slice-rest); + } + } + + @media (prefers-reduced-motion: reduce) { + .donut-slice { + animation: none; + } + } +`; + +const DonutCenter = styled.div` + align-items: center; + display: flex; + flex-direction: column; + inset: 0; + justify-content: center; + position: absolute; +`; + +const DonutValue = styled.span` + color: ${COLORS.text}; + font-family: ${APP_FONT}; + font-size: 22px; + font-weight: 600; + line-height: 1; +`; + +const DonutLabel = styled.span` + color: ${COLORS.textTertiary}; + font-family: ${APP_FONT}; + font-size: 11px; + line-height: 1.4; +`; + +const DonutLegend = styled.div` + display: flex; + flex: 1; + flex-direction: column; + gap: 6px; + min-width: 0; +`; + +const LegendRow = styled.div` + align-items: center; + display: flex; + gap: 6px; + min-width: 0; +`; + +const LegendDot = styled.span<{ $color: string }>` + background: ${({ $color }) => $color}; + border-radius: 2px; + flex-shrink: 0; + height: 8px; + width: 8px; +`; + +const LegendLabel = styled.span` + color: ${COLORS.textSecondary}; + font-family: ${APP_FONT}; + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export function DashboardDonutChart({ data }: { data: DashboardDonutChart }) { + const total = data.slices.reduce((sum, slice) => sum + slice.value, 0) || 1; + let cumulative = 0; + + return ( + + + + + {data.centerValue} + {data.centerLabel} + + + + {data.slices.map((slice) => ( + + + {slice.label} + + ))} + + + ); +} diff --git a/packages/twenty-website/src/sections/AppPreview/Pages/Dashboard/SalesDashboardPage.tsx b/packages/twenty-website/src/sections/AppPreview/Pages/Dashboard/SalesDashboardPage.tsx index 249525f20b..da604020db 100644 --- a/packages/twenty-website/src/sections/AppPreview/Pages/Dashboard/SalesDashboardPage.tsx +++ b/packages/twenty-website/src/sections/AppPreview/Pages/Dashboard/SalesDashboardPage.tsx @@ -1,40 +1,93 @@ -import type { DashboardChartImage, DashboardPageDefinition } from '../../types'; -import { theme } from '@/theme'; import { styled } from '@linaria/react'; -import { VISUAL_TOKENS } from '../../Shared/utils/app-preview-tokens'; +import { IconTrendingDown, IconTrendingUp } from '@tabler/icons-react'; -const APP_FONT = VISUAL_TOKENS.font.family; -const CARD_BACKGROUND = VISUAL_TOKENS.background.secondary; -const CARD_BORDER = VISUAL_TOKENS.border.color.light; +import type { DashboardKpi, DashboardPageDefinition } from '../../types'; +import { theme } from '@/theme'; +import { + SkeletonBar, + SkeletonBlock, +} from '../../Shared/components/PreviewSkeleton'; +import { APP_FONT, COLORS } from '../../Shared/utils/app-preview-theme'; +import { + DashboardBarChart, + DashboardDonutChart, + DashboardLineChart, +} from './DashboardCharts'; + +const TREND_UP_COLOR = '#53b9ab'; +const TREND_DOWN_COLOR = '#eb8e90'; const DashboardGrid = styled.div` display: grid; gap: 8px; grid-template-areas: - 'metrics' - 'visits' - 'revenue' - 'distribution'; + 'kpis' + 'line' + 'bar' + 'donut'; grid-template-columns: minmax(0, 1fr); - min-width: 0; + height: 100%; + min-height: 0; padding: 8px; @media (min-width: ${theme.breakpoints.md}px) { grid-template-areas: - 'metrics visits visits' - 'revenue revenue distribution'; - grid-template-columns: minmax(168px, 252px) minmax(0, 1fr) minmax( - 168px, - 252px + 'kpis line line' + 'bar bar donut'; + grid-template-columns: minmax(176px, 248px) minmax(0, 1fr) minmax( + 176px, + 248px ); + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); } `; -const MetricStack = styled.div` - align-self: stretch; +const WidgetCard = styled.div` + animation: dashboardWidgetAppear 360ms cubic-bezier(0.22, 1, 0.36, 1) both; + background: ${COLORS.backgroundSecondary}; + border: 1px solid ${COLORS.borderLight}; + border-radius: 8px; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 8px; + min-height: 0; + overflow: hidden; + padding: 8px; + + @keyframes dashboardWidgetAppear { + from { + opacity: 0; + transform: translateY(6px) scale(0.99); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } + } + + @media (prefers-reduced-motion: reduce) { + animation: none; + } +`; + +const WidgetTitle = styled.span` + color: ${COLORS.text}; + flex-shrink: 0; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: 500; + line-height: 1.4; + overflow: hidden; + padding: 0 2px; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const KpiStack = styled.div` display: grid; gap: 8px; - grid-area: metrics; + grid-area: kpis; min-width: 0; @media (min-width: ${theme.breakpoints.md}px) { @@ -42,76 +95,87 @@ const MetricStack = styled.div` } `; -const MetricCard = styled.div` - background: ${CARD_BACKGROUND}; - border: 1px solid ${CARD_BORDER}; - border-radius: 8px; +const KpiCard = styled(WidgetCard)` + gap: 6px; + justify-content: center; +`; + +const KpiValueRow = styled.div` + align-items: baseline; display: flex; - flex-direction: column; - gap: 16px; + gap: 8px; justify-content: space-between; - min-height: 92px; - padding: 12px; - - @media (min-width: ${theme.breakpoints.md}px) { - min-height: 0; - } `; -const MetricTitle = styled.span` - color: ${VISUAL_TOKENS.font.color.primary}; +const KpiValue = styled.span` + color: ${COLORS.text}; font-family: ${APP_FONT}; - font-size: 13px; - font-weight: ${theme.font.weight.medium}; - line-height: 1.4; -`; - -const MetricValue = styled.span` - color: ${VISUAL_TOKENS.font.color.primary}; - font-family: ${APP_FONT}; - font-size: 24px; + font-size: 25px; font-weight: 600; - line-height: 1.2; + line-height: 1.1; `; -const VisitsChart = styled.div` - grid-area: visits; - min-width: 0; +const KpiTrend = styled.span<{ $up: boolean }>` + align-items: center; + color: ${({ $up }) => ($up ? TREND_UP_COLOR : TREND_DOWN_COLOR)}; + display: inline-flex; + flex-shrink: 0; + font-family: ${APP_FONT}; + font-size: 12px; + font-weight: 500; + gap: 2px; `; -const RevenueChart = styled.div` - grid-area: revenue; - min-width: 0; +const LineCard = styled(WidgetCard)` + grid-area: line; `; -const DistributionChart = styled.div` - grid-area: distribution; - min-width: 0; +const BarCard = styled(WidgetCard)` + grid-area: bar; `; -const ChartImage = styled.img` - display: block; - height: auto; - max-width: 100%; - width: 100%; +const DonutCard = styled(WidgetCard)` + grid-area: donut; `; -function DashboardChart({ - chart, - className, -}: { - chart: DashboardChartImage; - className?: string; -}) { +function KpiWidget({ kpi }: { kpi: DashboardKpi }) { + const isUp = kpi.trend?.direction === 'up'; + return ( - + + {kpi.title} + + {kpi.value} + {kpi.trend ? ( + + {isUp ? ( + + ) : ( + + )} + {kpi.trend.value} + + ) : null} + + + ); +} + +function KpiSkeleton() { + return ( + + + + + ); +} + +function ChartSkeleton() { + return ( + <> + + + ); } @@ -120,28 +184,60 @@ export function SalesDashboardPage({ }: { page: DashboardPageDefinition; }) { + const { kpis, lineChart, barChart, donutChart, generating } = page.dashboard; + return ( - - {page.dashboard.metrics.map((metric) => ( - - {metric.title} - {metric.value} - - ))} - + {kpis.length > 0 ? ( + + {kpis.map((kpi) => + generating ? ( + + ) : ( + + ), + )} + + ) : null} - - - + {lineChart ? ( + + {generating ? ( + + ) : ( + <> + {lineChart.title} + + + )} + + ) : null} - - - + {barChart ? ( + + {generating ? ( + + ) : ( + <> + {barChart.title} + + + )} + + ) : null} - - - + {donutChart ? ( + + {generating ? ( + + ) : ( + <> + {donutChart.title} + + + )} + + ) : null} ); } diff --git a/packages/twenty-website/src/sections/AppPreview/Pages/Kanban/KanbanCard.tsx b/packages/twenty-website/src/sections/AppPreview/Pages/Kanban/KanbanCard.tsx index f82b324141..6b993674d9 100644 --- a/packages/twenty-website/src/sections/AppPreview/Pages/Kanban/KanbanCard.tsx +++ b/packages/twenty-website/src/sections/AppPreview/Pages/Kanban/KanbanCard.tsx @@ -16,6 +16,7 @@ import type { CellEntity, CellPerson, KanbanCard } from '../../types'; import { FaviconLogo } from '../../Shared/components/FaviconLogo'; import { PersonAvatar } from '../../Shared/components/PersonAvatar'; import { Chip } from '../../Shared/components/Chip'; +import { ChipVariant } from '../../Shared/utils/chip-variant'; import { KANBAN_PAGE_COLORS, KANBAN_PAGE_FONT, @@ -23,6 +24,7 @@ import { } from './kanban-page-theme'; const Card = styled.div` + animation: kanbanCardAppear 320ms cubic-bezier(0.22, 1, 0.36, 1) both; background: ${KANBAN_PAGE_COLORS.backgroundSecondary}; border: 1px solid ${KANBAN_PAGE_COLORS.border}; border-radius: 4px; @@ -30,6 +32,17 @@ const Card = styled.div` display: flex; flex-direction: column; overflow: hidden; + + @keyframes kanbanCardAppear { + from { + opacity: 0; + transform: translateY(6px) scale(0.985); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } + } `; const CardHeader = styled.div` @@ -145,6 +158,7 @@ function EntityChip({ entity }: { entity: CellEntity }) { label={entity.name} leftComponent={} maxWidth={152} + variant={ChipVariant.Highlighted} /> ); } @@ -156,6 +170,7 @@ function PersonChip({ person }: { person: CellPerson }) { label={person.name} leftComponent={} maxWidth={152} + variant={ChipVariant.Highlighted} /> ); } diff --git a/packages/twenty-website/src/sections/AppPreview/Pages/Kanban/KanbanLane.tsx b/packages/twenty-website/src/sections/AppPreview/Pages/Kanban/KanbanLane.tsx index 1da47d97f0..e8b70715c7 100644 --- a/packages/twenty-website/src/sections/AppPreview/Pages/Kanban/KanbanLane.tsx +++ b/packages/twenty-website/src/sections/AppPreview/Pages/Kanban/KanbanLane.tsx @@ -3,6 +3,10 @@ import { styled } from '@linaria/react'; import { IconPlus } from '@tabler/icons-react'; import type { KanbanLane } from '../../types'; +import { + SkeletonBar, + SkeletonCircle, +} from '../../Shared/components/PreviewSkeleton'; import { KanbanCard } from './KanbanCard'; import { KANBAN_LANE_TONES, @@ -88,16 +92,66 @@ const AddCardButton = styled.div` white-space: nowrap; `; +const SkeletonCardShell = styled.div<{ $index: number }>` + animation: kanbanSkeletonCardAppear 320ms cubic-bezier(0.22, 1, 0.36, 1) both; + animation-delay: ${({ $index }) => `${$index * 90}ms`}; + background: ${KANBAN_PAGE_COLORS.backgroundSecondary}; + border: 1px solid ${KANBAN_PAGE_COLORS.border}; + border-radius: 4px; + box-shadow: ${KANBAN_PAGE_COLORS.shadow}; + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px 8px; + + @keyframes kanbanSkeletonCardAppear { + from { + opacity: 0; + transform: translateY(6px) scale(0.985); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } + } +`; + +const SkeletonCardField = styled.div` + align-items: center; + display: flex; + gap: 6px; + width: 100%; +`; + +const SKELETON_FIELD_WIDTHS = ['70%', '54%', '62%']; + +function SkeletonCard({ index }: { index: number }) { + return ( + + + {SKELETON_FIELD_WIDTHS.map((width, fieldIndex) => ( + + + + + ))} + + ); +} + export function KanbanLane({ lane, index = 0, isLast, + generating = false, }: { + generating?: boolean; index?: number; isLast: boolean; lane: KanbanLane; }) { const tone = KANBAN_LANE_TONES[lane.tone] ?? KANBAN_LANE_TONES.gray; + const skeletonCardCount = 2 + (index % 2); return ( @@ -105,13 +159,15 @@ export function KanbanLane({ {lane.label} - {lane.cards.length} + {generating ? null : {lane.cards.length}} - {lane.cards.map((card) => ( - - ))} + {generating + ? Array.from({ length: skeletonCardCount }, (_, cardIndex) => ( + + )) + : lane.cards.map((card) => )}