diff --git a/packages/twenty-website-new/public/images/home/hero/sales-dashboard/distribution.png b/packages/twenty-website-new/public/images/home/hero/sales-dashboard/distribution.png new file mode 100644 index 0000000000..b456bde12b Binary files /dev/null and b/packages/twenty-website-new/public/images/home/hero/sales-dashboard/distribution.png differ diff --git a/packages/twenty-website-new/public/images/home/hero/sales-dashboard/revenue.png b/packages/twenty-website-new/public/images/home/hero/sales-dashboard/revenue.png new file mode 100644 index 0000000000..7fced1141c Binary files /dev/null and b/packages/twenty-website-new/public/images/home/hero/sales-dashboard/revenue.png differ diff --git a/packages/twenty-website-new/public/images/home/hero/sales-dashboard/visits.png b/packages/twenty-website-new/public/images/home/hero/sales-dashboard/visits.png new file mode 100644 index 0000000000..b5822a6f71 Binary files /dev/null and b/packages/twenty-website-new/public/images/home/hero/sales-dashboard/visits.png differ diff --git a/packages/twenty-website-new/src/app/(home)/_constants/hero.ts b/packages/twenty-website-new/src/app/(home)/_constants/hero.ts index 6fd6484acb..32625a25d8 100644 --- a/packages/twenty-website-new/src/app/(home)/_constants/hero.ts +++ b/packages/twenty-website-new/src/app/(home)/_constants/hero.ts @@ -1,20 +1,317 @@ -import type { HeroHomeDataType } from '@/sections/Hero/types'; +import type { + HeroDashboardDataType, + HeroDashboardPageDefinition, + HeroHomeDataType, + HeroKanbanPageDefinition, + HeroTablePageDefinition, +} from '@/sections/Hero/types'; const PEOPLE_AVATAR_URLS = { craigFederighi: 'https://twentyhq.github.io/placeholder-images/people/image-33.png', - eddyCue: - 'https://twentyhq.github.io/placeholder-images/people/image-18.png', + eddyCue: 'https://twentyhq.github.io/placeholder-images/people/image-18.png', jeffWilliams: 'https://twentyhq.github.io/placeholder-images/people/image-22.png', - katherineAdams: - '/images/home/hero/avatars/katherine-adams.jpg', + katherineAdams: '/images/home/hero/avatars/katherine-adams.jpg', philSchiller: 'https://twentyhq.github.io/placeholder-images/people/image-14.png', - timCook: - 'https://twentyhq.github.io/placeholder-images/people/image-27.png', + timCook: 'https://twentyhq.github.io/placeholder-images/people/image-27.png', } as const; +const SALES_DASHBOARD_DATA: HeroDashboardDataType = { + 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.png', + width: 2316, + }, + revenueChart: { + alt: 'Revenue line chart by month', + height: 1116, + src: '/images/home/hero/sales-dashboard/revenue.png', + width: 2316, + }, + distributionChart: { + alt: 'Ultra versus Plus donut chart distribution', + height: 1116, + src: '/images/home/hero/sales-dashboard/distribution.png', + width: 756, + }, +}; + +const SALES_DASHBOARD_PAGE: HeroDashboardPageDefinition = { + type: 'dashboard', + header: { + title: 'Sales Dashboard', + }, + dashboard: SALES_DASHBOARD_DATA, +}; + +const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = { + 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: 'Phil Schiller', + tone: 'amber', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.philSchiller, + }, + rating: 2, + date: 'Jul 1, 2023', + mainContact: { + type: 'person', + name: 'Dario Amodei', + shortLabel: 'D', + tone: 'gray', + kind: 'person', + avatarUrl: + 'https://twentyhq.github.io/placeholder-images/people/image-11.png', + }, + 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: 'Tim Cook', + tone: 'teal', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.timCook, + }, + rating: 2, + date: 'Jul 12, 2023', + mainContact: { + type: 'person', + name: 'Dylan Field', + shortLabel: 'D', + tone: 'purple', + kind: 'person', + avatarUrl: + 'https://twentyhq.github.io/placeholder-images/people/image-31.png', + }, + 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: 'Phil Schiller', + tone: 'amber', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.philSchiller, + }, + rating: 4, + date: 'Jul 8, 2023', + mainContact: { + type: 'person', + name: 'Ivan Zhao', + shortLabel: 'I', + tone: 'gray', + kind: 'person', + avatarUrl: + 'https://twentyhq.github.io/placeholder-images/people/image-09.png', + }, + 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: 'Jeff Williams', + tone: 'blue', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams, + }, + rating: 3, + date: 'Jul 14, 2023', + mainContact: { + type: 'person', + name: 'Thomas Dohmke', + shortLabel: 'T', + tone: 'gray', + kind: 'person', + avatarUrl: + 'https://twentyhq.github.io/placeholder-images/people/image-24.png', + }, + 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: 'Tim Cook', + tone: 'teal', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.timCook, + }, + rating: 5, + date: 'Jul 17, 2023', + mainContact: { + type: 'person', + name: 'Patrick Collison', + shortLabel: 'P', + tone: 'blue', + kind: 'person', + avatarUrl: + 'https://twentyhq.github.io/placeholder-images/people/image-16.png', + }, + 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: 'Eddy Cue', + tone: 'gray', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.eddyCue, + }, + rating: 3, + date: 'Jul 15, 2023', + mainContact: { + type: 'person', + name: 'Brian Chesky', + shortLabel: 'B', + tone: 'pink', + kind: 'person', + avatarUrl: + 'https://twentyhq.github.io/placeholder-images/people/image-01.png', + }, + 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: 'Craig Federighi', + tone: 'purple', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi, + }, + rating: 4, + date: 'Jul 23, 2023', + mainContact: { + type: 'person', + name: 'Rania Succar', + shortLabel: 'R', + tone: 'amber', + kind: 'person', + avatarUrl: + 'https://twentyhq.github.io/placeholder-images/people/image-28.png', + }, + recordId: 'OPP-7', + }, + ], + }, + ], +}; + +function createTablePage({ + title, + count, + columns, + rows, +}: { + title: string; + count: number; + columns: HeroTablePageDefinition['columns']; + rows: HeroTablePageDefinition['rows']; +}): HeroTablePageDefinition { + return { + type: 'table', + header: { + title, + count, + }, + columns, + rows, + }; +} + export const HERO_DATA: HeroHomeDataType = { heading: [ { text: 'Build', fontFamily: 'sans' }, @@ -28,611 +325,1189 @@ export const HERO_DATA: HeroHomeDataType = { 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, - viewLabel: 'All Companies', - viewCount: 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: 'qonto.com' }, - createdBy: { type: 'person', name: 'Jeff Williams', tone: 'blue', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams }, - address: { type: 'text', value: '18 Rue De Navarin' }, - accountOwner: { type: 'person', name: 'Phil Schiller', tone: 'amber', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.philSchiller }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-11.png' }, - employees: { type: 'number', value: '612' }, - opportunities: { type: 'relation', items: [{ name: 'Enterprise Expansion', shortLabel: 'E', tone: 'blue' }] }, - added: { type: 'text', value: 'Jul 1, 2023' }, + page: createTablePage({ + title: 'All Companies', + count: 9, + columns: [ + { + id: 'company', + label: 'Companies', + width: 180, + isFirstColumn: true, }, - }, - { - id: 'linkedin', - cells: { - company: { type: 'entity', name: 'Linkedin', domain: 'linkedin.com' }, - url: { type: 'link', value: 'linkedin.com' }, - createdBy: { type: 'person', name: 'Craig Federighi', tone: 'purple', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi }, - address: { type: 'text', value: '1226 Moises Causeway' }, - accountOwner: { type: 'person', name: 'Craig Federighi', tone: 'purple', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-26.png' }, - 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: '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: 'qonto.com' }, + createdBy: { + type: 'person', + name: 'Jeff Williams', + tone: 'blue', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams, + }, + address: { type: 'text', value: '18 Rue De Navarin' }, + accountOwner: { + type: 'person', + name: 'Phil Schiller', + tone: 'amber', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.philSchiller, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-11.png', + }, + employees: { type: 'number', value: '612' }, + opportunities: { + type: 'relation', + items: [ + { + name: 'Enterprise Expansion', + shortLabel: 'E', + tone: 'blue', + }, + ], + }, + added: { type: 'text', value: 'Jul 1, 2023' }, + }, }, - }, - { - id: 'slack', - cells: { - company: { type: 'entity', name: 'Slack', domain: 'slack.com' }, - url: { type: 'link', value: 'slack.com' }, - createdBy: { type: 'person', name: 'Eddy Cue', tone: 'gray', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.eddyCue }, - address: { type: 'text', value: '1316 Dameon Mountain' }, - accountOwner: { type: 'person', name: 'Katherine Adams', tone: 'red', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.katherineAdams }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-04.png' }, - 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: 'linkedin', + cells: { + company: { + type: 'entity', + name: 'Linkedin', + domain: 'linkedin.com', + }, + url: { type: 'link', value: 'linkedin.com' }, + createdBy: { + type: 'person', + name: 'Craig Federighi', + tone: 'purple', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi, + }, + address: { type: 'text', value: '1226 Moises Causeway' }, + accountOwner: { + type: 'person', + name: 'Craig Federighi', + tone: 'purple', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-26.png', + }, + 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: '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: 'Phil Schiller', tone: 'amber', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.philSchiller }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-09.png' }, - employees: { type: 'number', value: '620' }, - opportunities: { type: 'relation', items: [{ name: 'Workspace Consolidation', shortLabel: 'W', tone: 'gray' }] }, - added: { type: 'text', value: 'Jul 8, 2023' }, + { + id: 'slack', + cells: { + company: { type: 'entity', name: 'Slack', domain: 'slack.com' }, + url: { type: 'link', value: 'slack.com' }, + createdBy: { + type: 'person', + name: 'Eddy Cue', + tone: 'gray', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.eddyCue, + }, + address: { type: 'text', value: '1316 Dameon Mountain' }, + accountOwner: { + type: 'person', + name: 'Katherine Adams', + tone: 'red', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.katherineAdams, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-04.png', + }, + 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: '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: 'Tim Cook', tone: 'teal', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.timCook }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-31.png' }, - 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: '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: 'Phil Schiller', + tone: 'amber', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.philSchiller, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-09.png', + }, + employees: { type: 'number', value: '620' }, + opportunities: { + type: 'relation', + items: [ + { + name: 'Workspace Consolidation', + shortLabel: 'W', + tone: 'gray', + }, + ], + }, + added: { type: 'text', value: 'Jul 8, 2023' }, + }, }, - }, - { - id: 'github', - cells: { - company: { type: 'entity', name: 'Github', domain: 'github.com' }, - url: { type: 'link', value: 'github.com' }, - createdBy: { type: 'person', name: 'Jeff Williams', tone: 'blue', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams }, - address: { type: 'text', value: '3891 Ranchview Drive' }, - accountOwner: { type: 'person', name: 'Jeff Williams', tone: 'blue', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-24.png' }, - 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: '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: 'Tim Cook', + tone: 'teal', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.timCook, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-31.png', + }, + 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: 'airbnb', - cells: { - company: { type: 'entity', name: 'Airbnb', domain: 'airbnb.com' }, - url: { type: 'link', value: 'airbnb.com' }, - createdBy: { type: 'person', name: 'Tim Cook', tone: 'teal', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.timCook }, - address: { type: 'text', value: '4517 Washington Avenue' }, - accountOwner: { type: 'person', name: 'Eddy Cue', tone: 'gray', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.eddyCue }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-01.png' }, - 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: 'github', + cells: { + company: { + type: 'entity', + name: 'Github', + domain: 'github.com', + }, + url: { type: 'link', value: 'github.com' }, + createdBy: { + type: 'person', + name: 'Jeff Williams', + tone: 'blue', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams, + }, + address: { type: 'text', value: '3891 Ranchview Drive' }, + accountOwner: { + type: 'person', + name: 'Jeff Williams', + tone: 'blue', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-24.png', + }, + 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: 'stripe', - cells: { - company: { type: 'entity', name: 'Stripe', domain: 'stripe.com' }, - url: { type: 'link', value: 'stripe.com' }, - createdBy: { type: 'person', name: 'Katherine Adams', tone: 'red', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.katherineAdams }, - address: { type: 'text', value: '2118 Thornridge Circle' }, - accountOwner: { type: 'person', name: 'Tim Cook', tone: 'teal', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.timCook }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-16.png' }, - 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: 'airbnb', + cells: { + company: { + type: 'entity', + name: 'Airbnb', + domain: 'airbnb.com', + }, + url: { type: 'link', value: 'airbnb.com' }, + createdBy: { + type: 'person', + name: 'Tim Cook', + tone: 'teal', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.timCook, + }, + address: { type: 'text', value: '4517 Washington Avenue' }, + accountOwner: { + type: 'person', + name: 'Eddy Cue', + tone: 'gray', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.eddyCue, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-01.png', + }, + 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: 'sequoia', - cells: { - company: { type: 'entity', name: 'Sequoia', domain: 'sequoia.com' }, - url: { type: 'link', value: 'sequoia.com' }, - createdBy: { type: 'person', name: 'Phil Schiller', tone: 'amber', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.philSchiller }, - address: { type: 'text', value: '1316 Dameon Mountain' }, - accountOwner: { type: 'person', name: 'Phil Schiller', tone: 'amber', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.philSchiller }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-13.png' }, - 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: 'stripe', + cells: { + company: { + type: 'entity', + name: 'Stripe', + domain: 'stripe.com', + }, + url: { type: 'link', value: 'stripe.com' }, + createdBy: { + type: 'person', + name: 'Katherine Adams', + tone: 'red', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.katherineAdams, + }, + address: { type: 'text', value: '2118 Thornridge Circle' }, + accountOwner: { + type: 'person', + name: 'Tim Cook', + tone: 'teal', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.timCook, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-16.png', + }, + 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: 'segment', - cells: { - company: { type: 'entity', name: 'Segment', domain: 'segment.com' }, - url: { type: 'link', value: 'segment.com' }, - createdBy: { type: 'person', name: 'Phil Schiller', tone: 'amber', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.philSchiller }, - address: { type: 'text', value: '8502 Preston Rd. East' }, - accountOwner: { type: 'person', name: 'Eddy Cue', tone: 'gray', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.eddyCue }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-20.png' }, - 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: 'sequoia', + cells: { + company: { + type: 'entity', + name: 'Sequoia', + domain: 'sequoia.com', + }, + url: { type: 'link', value: 'sequoia.com' }, + createdBy: { + type: 'person', + name: 'Phil Schiller', + tone: 'amber', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.philSchiller, + }, + address: { type: 'text', value: '1316 Dameon Mountain' }, + accountOwner: { + type: 'person', + name: 'Phil Schiller', + tone: 'amber', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.philSchiller, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-13.png', + }, + 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: 'mailchimp', - cells: { - company: { type: 'entity', name: 'Mailchimp', domain: 'mailchimp.com' }, - url: { type: 'link', value: 'mailchimp.com' }, - createdBy: { type: 'person', name: 'Eddy Cue', tone: 'gray', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.eddyCue }, - address: { type: 'text', value: '3517 W. Gray St.' }, - accountOwner: { type: 'person', name: 'Craig Federighi', tone: 'purple', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-28.png' }, - 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: 'segment', + cells: { + company: { + type: 'entity', + name: 'Segment', + domain: 'segment.com', + }, + url: { type: 'link', value: 'segment.com' }, + createdBy: { + type: 'person', + name: 'Phil Schiller', + tone: 'amber', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.philSchiller, + }, + address: { type: 'text', value: '8502 Preston Rd. East' }, + accountOwner: { + type: 'person', + name: 'Eddy Cue', + tone: 'gray', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.eddyCue, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-20.png', + }, + 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: 'accel', - cells: { - company: { type: 'entity', name: 'Accel', domain: 'accel.com' }, - url: { type: 'link', value: 'accel.com' }, - createdBy: { type: 'person', name: 'Craig Federighi', tone: 'purple', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi }, - address: { type: 'text', value: '4140 Parker Rd.' }, - accountOwner: { type: 'person', name: 'Katherine Adams', tone: 'red', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.katherineAdams }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-30.png' }, - employees: { type: 'number', value: '540' }, - opportunities: { type: 'relation', items: [{ name: 'Portfolio Sync', shortLabel: 'P', tone: 'purple' }] }, - added: { type: 'text', value: 'Jul 24, 2023' }, + { + id: 'mailchimp', + cells: { + company: { + type: 'entity', + name: 'Mailchimp', + domain: 'mailchimp.com', + }, + url: { type: 'link', value: 'mailchimp.com' }, + createdBy: { + type: 'person', + name: 'Eddy Cue', + tone: 'gray', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.eddyCue, + }, + address: { type: 'text', value: '3517 W. Gray St.' }, + accountOwner: { + type: 'person', + name: 'Craig Federighi', + tone: 'purple', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-28.png', + }, + 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: '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: 'Tim Cook', tone: 'teal', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.timCook }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-05.png' }, - employees: { type: 'number', value: '734' }, - opportunities: { type: 'relation', items: [{ name: 'Fundraising', shortLabel: 'F', tone: 'gray' }] }, - added: { type: 'text', value: 'Jul 25, 2023' }, + { + id: 'accel', + cells: { + company: { type: 'entity', name: 'Accel', domain: 'accel.com' }, + url: { type: 'link', value: 'accel.com' }, + createdBy: { + type: 'person', + name: 'Craig Federighi', + tone: 'purple', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi, + }, + address: { type: 'text', value: '4140 Parker Rd.' }, + accountOwner: { + type: 'person', + name: 'Katherine Adams', + tone: 'red', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.katherineAdams, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-30.png', + }, + employees: { type: 'number', value: '540' }, + opportunities: { + type: 'relation', + items: [ + { name: 'Portfolio Sync', shortLabel: 'P', tone: 'purple' }, + ], + }, + added: { type: 'text', value: 'Jul 24, 2023' }, + }, }, - }, - { - id: 'google', - cells: { - company: { type: 'entity', name: 'Google', domain: 'google.com' }, - url: { type: 'link', value: 'google.com' }, - createdBy: { type: 'person', name: 'Tim Cook', tone: 'teal', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.timCook }, - address: { type: 'text', value: '4140 Parker Rd.' }, - accountOwner: { type: 'person', name: 'Jeff Williams', tone: 'blue', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-32.png' }, - 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: '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: 'Tim Cook', + tone: 'teal', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.timCook, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-05.png', + }, + 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: 'Tim Cook', + tone: 'teal', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.timCook, + }, + address: { type: 'text', value: '4140 Parker Rd.' }, + accountOwner: { + type: 'person', + name: 'Jeff Williams', + tone: 'blue', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams, + }, + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-32.png', + }, + 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' }, - viewLabel: 'All People', - viewCount: 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: 'jeff-williams', - cells: { - name: { type: 'person', name: 'Jeff Williams', tone: 'blue', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams }, - company: { type: 'entity', name: 'Anthropic', domain: 'anthropic.com' }, - email: { type: 'link', value: 'jeff@anthropic.com' }, - phone: { type: 'text', value: '+33 1 23 45 67 89' }, - jobTitle: { type: 'text', value: 'COO' }, - city: { type: 'text', value: 'Paris' }, - linkedin: { type: 'link', value: 'jeff-williams' }, - added: { type: 'text', value: 'Jul 3, 2023' }, + 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: 'jeff-williams', + cells: { + name: { + type: 'person', + name: 'Jeff Williams', + tone: 'blue', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams, + }, + company: { + type: 'entity', + name: 'Anthropic', + domain: 'anthropic.com', + }, + email: { type: 'link', value: 'jeff@anthropic.com' }, + phone: { type: 'text', value: '+33 1 23 45 67 89' }, + jobTitle: { type: 'text', value: 'COO' }, + city: { type: 'text', value: 'Paris' }, + linkedin: { type: 'link', value: 'jeff-williams' }, + added: { type: 'text', value: 'Jul 3, 2023' }, + }, }, - }, - { - id: 'craig-federighi', - cells: { - name: { type: 'person', name: 'Craig Federighi', tone: 'purple', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi }, - company: { type: 'entity', name: 'Linkedin', domain: 'linkedin.com' }, - email: { type: 'link', value: 'craig@linkedin.com' }, - phone: { type: 'text', value: '+1 234 567 8900' }, - jobTitle: { type: 'text', value: 'SVP Software Engineering' }, - city: { type: 'text', value: 'Tabithaville' }, - linkedin: { type: 'link', value: 'craig-federighi' }, - added: { type: 'text', value: 'Jul 28, 2023' }, + { + id: 'craig-federighi', + cells: { + name: { + type: 'person', + name: 'Craig Federighi', + tone: 'purple', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi, + }, + company: { + type: 'entity', + name: 'Linkedin', + domain: 'linkedin.com', + }, + email: { type: 'link', value: 'craig@linkedin.com' }, + phone: { type: 'text', value: '+1 234 567 8900' }, + jobTitle: { type: 'text', value: 'SVP Software Engineering' }, + city: { type: 'text', value: 'Tabithaville' }, + linkedin: { type: 'link', value: 'craig-federighi' }, + added: { type: 'text', value: 'Jul 28, 2023' }, + }, }, - }, - ], + ], + }), }, { id: 'opportunities', label: 'Opportunities', icon: { kind: 'tabler', name: 'targetArrow', tone: 'red' }, - viewLabel: 'All Opportunities', - viewCount: 3, - columns: [ - { id: 'name', label: 'Name', width: 200, isFirstColumn: true }, - { id: 'company', label: 'Company', width: 160 }, - { id: 'amount', label: 'Amount', width: 140, align: 'right' }, - { id: 'stage', label: 'Stage', width: 140 }, - { id: 'expectedCloseDate', label: 'Expected Close Date', width: 180 }, - { id: 'accountOwner', label: 'Account Owner', width: 160 }, - ], - rows: [ - { - id: 'enterprise-rollout', - cells: { - name: { - type: 'text', - value: 'Enterprise rollout', - shortLabel: 'E', - tone: 'pink', - }, - company: { type: 'entity', name: 'Slack', domain: 'slack.com' }, - amount: { type: 'number', value: '$2,300,000' }, - stage: { type: 'tag', value: 'Negotiation' }, - expectedCloseDate: { type: 'text', value: 'Dec 15, 2023' }, - accountOwner: { type: 'person', name: 'Katherine Adams', tone: 'red', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.katherineAdams }, - }, - }, - { - id: 'workspace-upgrade', - cells: { - name: { - type: 'text', - value: 'Workspace upgrade', - shortLabel: 'W', - tone: 'pink', - }, - company: { type: 'entity', name: 'Notion', domain: 'notion.com' }, - amount: { type: 'number', value: '$750,000' }, - stage: { type: 'tag', value: 'Qualified' }, - expectedCloseDate: { type: 'text', value: 'Nov 30, 2023' }, - accountOwner: { type: 'person', name: 'Phil Schiller', tone: 'amber', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.philSchiller }, - }, - }, - ], + page: OPPORTUNITY_KANBAN_PAGE, }, { id: 'tasks', label: 'Tasks', icon: { kind: 'tabler', name: 'checkbox', tone: 'teal' }, - viewLabel: 'All Tasks', - viewCount: 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', + 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: 'Tim Cook', + tone: 'teal', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.timCook, + }, + dueDate: { type: 'text', value: 'Oct 25, 2023' }, + relatedTo: { + type: 'entity', + name: 'Anthropic', + domain: 'anthropic.com', + }, + status: { type: 'tag', value: 'To Do' }, }, - assignee: { type: 'person', name: 'Tim Cook', tone: 'teal', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.timCook }, - 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', + { + id: 'review-proposal', + cells: { + title: { + type: 'text', + value: 'Review proposal', + shortLabel: 'R', + tone: 'teal', + }, + assignee: { + type: 'person', + name: 'Eddy Cue', + tone: 'gray', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.eddyCue, + }, + dueDate: { type: 'text', value: 'Oct 28, 2023' }, + relatedTo: { + type: 'entity', + name: 'Slack', + domain: 'slack.com', + }, + status: { type: 'tag', value: 'In Progress' }, }, - assignee: { type: 'person', name: 'Eddy Cue', tone: 'gray', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.eddyCue }, - 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' }, - viewLabel: 'All Notes', - viewCount: 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', + 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: 'Phil Schiller', + tone: 'amber', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.philSchiller, + }, + relatedTo: { + type: 'entity', + name: 'Notion', + domain: 'notion.com', + }, + added: { type: 'text', value: 'Sep 2, 2023' }, }, - createdBy: { type: 'person', name: 'Phil Schiller', tone: 'amber', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.philSchiller }, - 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', + { + id: 'design-system-meeting', + cells: { + title: { + type: 'text', + value: 'Design system meeting', + shortLabel: 'D', + tone: 'green', + }, + createdBy: { + type: 'person', + name: 'Tim Cook', + tone: 'teal', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.timCook, + }, + relatedTo: { + type: 'entity', + name: 'Figma', + domain: 'figma.com', + }, + added: { type: 'text', value: 'Oct 18, 2023' }, }, - createdBy: { type: 'person', name: 'Tim Cook', tone: 'teal', kind: 'person', avatarUrl: PEOPLE_AVATAR_URLS.timCook }, - relatedTo: { type: 'entity', name: 'Figma', domain: 'figma.com' }, - added: { type: 'text', value: 'Oct 18, 2023' }, }, - }, - ], + ], + }), }, { - id: 'sales-dashboard', - label: 'Sales Dashboard', - icon: { kind: 'avatar', label: 'S', tone: 'amber', shape: 'circle' }, - meta: 'Dashboard', - viewLabel: 'Sales Dashboard', - viewCount: 0, - columns: [], - rows: [], + 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: 'Phil Schiller', + tone: 'amber', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.philSchiller, + }, + 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: 'Jeff Williams', + tone: 'blue', + kind: 'person', + avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams, + }, + added: { type: 'text', value: 'Oct 19, 2023' }, + }, + }, + ], + }), }, { id: 'workflows', label: 'Workflows', icon: { kind: 'tabler', name: 'settingsAutomation', tone: 'orange' }, showChevron: true, - children: [ + 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-list', label: 'All Workflows', icon: { kind: 'tabler', name: 'settingsAutomation', tone: 'gray' }, - viewLabel: 'All Workflows', - viewCount: 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: 'new-lead', - cells: { - name: { - type: 'text', - value: 'New Lead Assignment', - shortLabel: 'N', - tone: 'amber', + 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' }, }, - 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', + { + 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' }, }, - 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' }, - viewLabel: 'All Runs', - viewCount: 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' }, + page: createTablePage({ + title: 'All Runs', + count: 2, + columns: [ + { + id: 'runId', + label: 'Run ID', + width: 160, + isFirstColumn: true, }, - }, - { - id: 'run-12346', - cells: { - runId: { - type: 'text', - value: 'run_12346', - shortLabel: 'R', - tone: 'amber', + { 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' }, }, - 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: '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' }, - viewLabel: 'All Versions', - viewCount: 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: 'https://twentyhq.github.io/placeholder-images/people/image-09.png' }, + page: createTablePage({ + title: 'All Versions', + count: 2, + columns: [ + { + id: 'version', + label: 'Version', + width: 120, + isFirstColumn: true, }, - }, - { - id: 'v1-lead', - cells: { - version: { - type: 'text', - value: 'v1', - shortLabel: 'V', - tone: 'amber', + { 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: + 'https://twentyhq.github.io/placeholder-images/people/image-09.png', + }, }, - 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: 'https://twentyhq.github.io/placeholder-images/people/image-09.png' }, }, - }, - ], + { + 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: + 'https://twentyhq.github.io/placeholder-images/people/image-09.png', + }, + }, + }, + ], + }), }, ], }, diff --git a/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/HomeVisual.tsx b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/HomeVisual.tsx index aa585e97ad..d3fb143c2b 100644 --- a/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/HomeVisual.tsx +++ b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/HomeVisual.tsx @@ -1,10 +1,10 @@ 'use client'; +import dynamic from 'next/dynamic'; import { theme } from '@/theme'; import { styled } from '@linaria/react'; import { IconBook, - IconBox, IconBrandLinkedin, IconBuildingFactory2, IconBuildingSkyscraper, @@ -17,6 +17,8 @@ import { IconDotsVertical, IconFolder, IconHome2, + IconLayoutDashboard, + IconLayoutKanban, IconLayoutSidebarLeftCollapse, IconLink, IconList, @@ -26,8 +28,12 @@ import { IconMoneybag, IconNotes, IconPencil, + IconChevronUp, + IconHeart, + IconPlayerPause, IconPlayerPlay, IconPlus, + IconRepeat, IconSearch, IconSettings, IconSettingsAutomation, @@ -40,7 +46,6 @@ import { IconX, } from '@tabler/icons-react'; import { - useEffect, useMemo, useRef, useState, @@ -48,21 +53,30 @@ import { type PointerEvent as ReactPointerEvent, } from 'react'; import type { + HeroDashboardPageDefinition, HeroCellEntity, + HeroKanbanPageDefinition, HeroCellPerson, HeroCellRelation, HeroCellText, HeroCellValue, - HeroColumnDef, - HeroRowDef, + HeroNavbarActionType, + HeroPageDefinition, + HeroPageType, HeroSidebarEntry, HeroSidebarFolder, HeroSidebarIcon, HeroSidebarItem, + HeroTablePageDefinition, + HeroWorkflowPageDefinition, HeroVisualType, } from '../../types/HeroHomeData'; import { Chip, ChipVariant } from './homeVisualChip'; import { VISUAL_TOKENS } from './homeVisualTokens'; +import { normalizeHeroPage, type HeroPageDefaults } from './normalizeHeroPage'; +import { KanbanPage } from './KanbanPage'; +import { PagePreviewLoader } from './PagePreviewLoader'; +import { TablePage } from './TablePage'; const APP_FONT = VISUAL_TOKENS.font.family; const DEFAULT_TABLE_WIDTH = 1700; @@ -121,6 +135,57 @@ const ROW_HOVER_ACTION_DISABLED_COLUMNS = new Set([ 'accountOwner', ]); +const NAVBAR_ACTION_ICON_MAP: Record = { + chevronDown: IconChevronDown, + chevronUp: IconChevronUp, + dotsVertical: IconDotsVertical, + heart: IconHeart, + playerPause: IconPlayerPause, + plus: IconPlus, + repeat: IconRepeat, +}; + +const SalesDashboardPage = dynamic( + () => + import('./SalesDashboardPage').then((mod) => ({ + default: mod.SalesDashboardPage, + })), + { + loading: () => ( + + ), + ssr: false, + }, +); + +const WorkflowPage = dynamic( + () => + import('./WorkflowPage').then((mod) => ({ + default: mod.WorkflowPage, + })), + { + loading: () => , + ssr: false, + }, +); + +const PAGE_RENDERERS = { + table: (page: HeroTablePageDefinition) => , + kanban: (page: HeroKanbanPageDefinition) => , + dashboard: (page: HeroDashboardPageDefinition) => ( + + + + ), + workflow: (page: HeroWorkflowPageDefinition) => , +} satisfies { + [K in HeroPageType]: ( + page: Extract, + ) => ReactNode; +}; + // -- Styled Components -- const StyledHomeVisual = styled.div` @@ -137,7 +202,6 @@ const StyledHomeVisual = styled.div` const ShellScene = styled.div` margin: 0 auto; - max-width: 980px; transform-origin: center top; transition: transform 0.18s ease; width: 100%; @@ -150,8 +214,10 @@ const Frame = styled.div` border: 1px solid ${COLORS.border}; border-radius: 8px; box-shadow: ${COLORS.shadow}; + max-height: 740px; overflow: hidden; position: relative; + width: 100%; `; const AppLayout = styled.div` @@ -269,8 +335,7 @@ const SegmentedRail = styled.div` const Segment = styled.div<{ $selected?: boolean }>` align-items: center; - background: ${({ $selected }) => - $selected ? '#0000000a' : 'transparent'}; + background: ${({ $selected }) => ($selected ? '#0000000a' : 'transparent')}; border-radius: 16px; display: flex; height: 22px; @@ -329,6 +394,7 @@ const SidebarScroll = styled.div` const SidebarSection = styled.div` display: grid; gap: 2px; + padding-bottom: 8px; `; const SidebarSectionLabel = styled.span<{ $workspace?: boolean }>` @@ -338,8 +404,7 @@ const SidebarSectionLabel = styled.span<{ $workspace?: boolean }>` font-size: 11px; font-weight: 600; line-height: 1; - padding: ${({ $workspace }) => - $workspace ? '4px 4px 8px' : '0 4px 4px'}; + padding: ${({ $workspace }) => ($workspace ? '4px 4px 8px' : '0 4px 4px')}; @media (min-width: ${theme.breakpoints.md}px) { display: block; @@ -461,9 +526,11 @@ const SidebarItemMeta = styled.span` } `; -const SidebarChevron = styled.div` +const SidebarChevron = styled.div<{ $expanded?: boolean }>` color: ${COLORS.textTertiary}; display: none; + transform: rotate(${({ $expanded }) => ($expanded ? '0deg' : '-90deg')}); + transition: transform 0.16s ease; @media (min-width: ${theme.breakpoints.md}px) { display: flex; @@ -576,18 +643,6 @@ const BreadcrumbTag = styled.div` padding: 0 2px; `; -const AccentIconSurface = styled.div` - align-items: center; - background: ${COLORS.accentSurface}; - border: 1px solid ${COLORS.accentBorder}; - border-radius: 4px; - color: ${COLORS.accent}; - display: flex; - height: 16px; - justify-content: center; - width: 16px; -`; - const CrumbLabel = styled.span` color: ${COLORS.text}; font-family: ${APP_FONT}; @@ -617,7 +672,7 @@ const DesktopOnlyNavbarAction = styled.div` const NAVBAR_ACTION_BORDER = 'rgba(0, 0, 0, 0.08)'; -const NavbarActionButton = styled.div` +const NavbarActionButton = styled.div<{ $iconOnly?: boolean }>` align-items: center; background: transparent; border: 1px solid ${NAVBAR_ACTION_BORDER}; @@ -628,7 +683,10 @@ const NavbarActionButton = styled.div` font-weight: ${VISUAL_TOKENS.font.weight.medium}; gap: ${VISUAL_TOKENS.spacing[1]}; height: 24px; - padding: 0 ${VISUAL_TOKENS.spacing[2]}; + justify-content: center; + min-width: ${({ $iconOnly }) => ($iconOnly ? '24px' : 'auto')}; + padding: ${({ $iconOnly }) => + $iconOnly ? '0' : `0 ${VISUAL_TOKENS.spacing[2]}`}; white-space: nowrap; `; @@ -742,12 +800,25 @@ const ViewAction = styled.span` const TableShell = styled.div` display: flex; flex: 1 1 auto; - min-width: 0; min-height: 0; + min-width: 0; overflow: hidden; width: 100%; `; +const DashboardViewport = styled.div` + flex: 1 1 auto; + min-height: 0; + min-width: 0; + overflow: auto; + scrollbar-width: none; + width: 100%; + + &::-webkit-scrollbar { + display: none; + } +`; + const GripRail = styled.div` background: ${COLORS.background}; display: grid; @@ -1002,8 +1073,9 @@ const HoverActions = styled.div<{ $rightInset?: number; $visible: boolean }>` padding: 0 4px; pointer-events: none; position: absolute; - right: ${({ $rightInset = HOVER_ACTION_EDGE_INSET - TABLE_CELL_HORIZONTAL_PADDING }) => - `${$rightInset}px`}; + right: ${({ + $rightInset = HOVER_ACTION_EDGE_INSET - TABLE_CELL_HORIZONTAL_PADDING, + }) => `${$rightInset}px`}; top: 4px; transform: translateX(${({ $visible }) => ($visible ? '0' : '4px')}); transition: @@ -1075,10 +1147,10 @@ const FaviconImage = styled.img` const TABLER_ICON_MAP: Record = { book: IconBook, - box: IconBox, buildingSkyscraper: IconBuildingSkyscraper, checkbox: IconCheckbox, folder: IconFolder, + layoutDashboard: IconLayoutDashboard, notes: IconNotes, playerPlay: IconPlayerPlay, settings: IconSettings, @@ -1136,9 +1208,17 @@ function isFolder(entry: HeroSidebarEntry): entry is HeroSidebarFolder { return 'items' in entry; } +function hasRenderablePage( + item: HeroSidebarItem, + pageDefaults: HeroPageDefaults, +): boolean { + return normalizeHeroPage(item, pageDefaults) !== null; +} + function findActiveItem( entries: HeroSidebarEntry[], activeLabel: string, + pageDefaults: HeroPageDefaults, ): HeroSidebarItem | undefined { for (const entry of entries) { if (isFolder(entry)) { @@ -1160,17 +1240,17 @@ function findActiveItem( } if (entry.label === activeLabel) { - const entryHasTable = - (entry.columns?.length ?? 0) > 0 || (entry.rows?.length ?? 0) > 0; - - if (!entryHasTable && entry.children && entry.children.length > 0) { - const firstChildWithTable = entry.children.find( - (child) => - (child.columns?.length ?? 0) > 0 || (child.rows?.length ?? 0) > 0, + if ( + !hasRenderablePage(entry, pageDefaults) && + entry.children && + entry.children.length > 0 + ) { + const firstChildWithRenderablePage = entry.children.find((child) => + hasRenderablePage(child, pageDefaults), ); - if (firstChildWithTable) { - return firstChildWithTable; + if (firstChildWithRenderablePage) { + return firstChildWithRenderablePage; } } @@ -1181,6 +1261,109 @@ function findActiveItem( return undefined; } +function findContainingFolderId( + entries: HeroSidebarEntry[], + label: string, +): string | undefined { + for (const entry of entries) { + if (!isFolder(entry)) { + continue; + } + + if ( + entry.items.some( + (item) => + item.label === label || + item.children?.some((child) => child.label === label) === true, + ) + ) { + return entry.id; + } + } + + return undefined; +} + +function renderPageDefinition( + page: HeroPageDefinition, + onNavigateToLabel?: (label: string) => void, +) { + switch (page.type) { + case 'table': + return ; + case 'kanban': + return PAGE_RENDERERS.kanban(page); + case 'dashboard': + return PAGE_RENDERERS.dashboard(page); + case 'workflow': + return PAGE_RENDERERS.workflow(page); + } +} + +function getNavbarActionToneColor( + tone: HeroNavbarActionType['labelTone'], +): string { + if (tone === 'primary') { + return VISUAL_TOKENS.font.color.primary; + } + + if (tone === 'tertiary') { + return VISUAL_TOKENS.font.color.light; + } + + return VISUAL_TOKENS.font.color.secondary; +} + +function renderNavbarAction( + action: HeroNavbarActionType, + index: number, +): ReactNode { + const ActionIcon = NAVBAR_ACTION_ICON_MAP[action.icon]; + const isIconOnly = + action.variant === 'icon' || (!action.label && !action.trailingLabel); + const labelColor = getNavbarActionToneColor(action.labelTone); + + const button = ( + + {ActionIcon ? ( + + + + ) : null} + {action.label ? ( + + {action.label} + + ) : null} + {action.trailingLabel ? ( + <> + + + {action.trailingLabel} + + + ) : null} + + ); + + if (action.desktopOnly) { + return ( + + {button} + + ); + } + + return button; +} + // -- Small icon wrappers -- type MiniIconProps = { @@ -1269,6 +1452,20 @@ function ListMini({ color = COLORS.textSecondary, size = 16 }: MiniIconProps) { ); } +function KanbanMini({ + color = COLORS.textSecondary, + size = 16, +}: MiniIconProps) { + return ( + + ); +} + function PlusMini({ color = COLORS.textSecondary, size = 14 }: MiniIconProps) { return ( @@ -1435,7 +1632,7 @@ function renderSidebarIcon(icon: HeroSidebarIcon): ReactNode { return ( void; onSelect?: (label: string) => void; selectedLabel?: string; }) { const showBranch = depth > 0; - const rowSelectable = interactive && item.href === undefined; - const rowInteractive = rowSelectable || item.href !== undefined; + const rowSelectable = interactive && item.href === undefined && !collapsible; + const rowInteractive = + rowSelectable || item.href !== undefined || (interactive && collapsible); const rowActive = - rowSelectable && selectedLabel !== undefined && item.label === selectedLabel; + rowSelectable && + selectedLabel !== undefined && + item.label === selectedLabel; const childItems = item.children ?? []; const rowContent = ( <> @@ -1524,7 +1730,7 @@ function SidebarItemComponent({ {item.showChevron || (item.children && item.children.length > 0) ? ( - + ) : null} @@ -1552,13 +1758,19 @@ function SidebarItemComponent({ $depth={depth} $interactive={rowInteractive} $withBranch={showBranch} - onClick={rowSelectable ? () => onSelect?.(item.label) : undefined} + onClick={ + collapsible + ? onToggleExpanded + : rowSelectable + ? () => onSelect?.(item.label) + : undefined + } style={{ cursor: rowInteractive ? 'pointer' : 'default' }} > {rowContent} )} - {childItems.length > 0 ? ( + {childItems.length > 0 && (!collapsible || expanded) ? ( {childItems.map((child, index) => ( @@ -1717,10 +1929,7 @@ function TextCellComponent({ clickable={false} label={cell.value} leftComponent={ - + {cell.shortLabel} } @@ -1786,42 +1995,67 @@ function renderCellValue( export function HomeVisual({ visual }: { visual: HeroVisualType }) { const shellRef = useRef(null); - const viewportRef = useRef(null); - const dragRef = useRef({ - active: false, - pointerId: -1, - startScrollLeft: 0, - startX: 0, - }); const defaultActiveLabel = + visual.favoritesNav?.find((item) => item.active)?.label ?? visual.workspaceNav.find((entry) => !isFolder(entry) && entry.active) ?.label ?? visual.workspaceNav[0]?.label ?? ''; const [activeLabel, setActiveLabel] = useState(defaultActiveLabel); - const [dragging, setDragging] = useState(false); - const [hoveredRowId, setHoveredRowId] = useState(null); - const [tilt, setTilt] = useState({ x: 0, y: 0 }); + const [openFolderIds, setOpenFolderIds] = useState(() => { + const activeFolderId = findContainingFolderId( + visual.workspaceNav, + defaultActiveLabel, + ); - const activeItem = useMemo( - () => findActiveItem(visual.workspaceNav, activeLabel), - [activeLabel, visual.workspaceNav], + return visual.workspaceNav.flatMap((entry) => { + if (!isFolder(entry)) { + return []; + } + + if (entry.defaultOpen || entry.id === activeFolderId) { + return [entry.id]; + } + + return []; + }); + }); + const [tilt, setTilt] = useState({ x: 0, y: 0 }); + const pageDefaults = useMemo( + () => ({ + defaultActions: visual.actions ?? [], + defaultTableWidth: visual.tableWidth ?? DEFAULT_TABLE_WIDTH, + }), + [visual.actions, visual.tableWidth], ); - const columns: HeroColumnDef[] = activeItem?.columns ?? []; - const rows: HeroRowDef[] = activeItem?.rows ?? []; - const viewLabel = activeItem?.viewLabel ?? activeLabel; - const viewCount = activeItem?.viewCount ?? rows.length; - const totalTableWidth = visual.tableWidth ?? DEFAULT_TABLE_WIDTH; - const actions = visual.actions ?? []; - - const columnWidth = columns.reduce((sum, column) => sum + column.width, 0); - const fillerWidth = Math.max(totalTableWidth - columnWidth, 0); + const activeItem = useMemo( + () => + (visual.favoritesNav + ? findActiveItem(visual.favoritesNav, activeLabel, pageDefaults) + : undefined) ?? + findActiveItem(visual.workspaceNav, activeLabel, pageDefaults), + [activeLabel, pageDefaults, visual.favoritesNav, visual.workspaceNav], + ); + const activePage = useMemo( + () => (activeItem ? normalizeHeroPage(activeItem, pageDefaults) : null), + [activeItem, pageDefaults], + ); + const activeHeader = activePage?.header; + const activeActions = activeHeader?.actions ?? []; + const navbarActions = activeHeader?.navbarActions; + const showPageCount = activeHeader?.count !== undefined; + const showListIcon = activeHeader?.showListIcon ?? false; + const showViewBar = + activePage !== null && + activePage !== undefined && + activePage.type !== 'dashboard' && + activePage.type !== 'workflow'; const handleShellPointerMove = (event: ReactPointerEvent) => { - if (event.pointerType !== 'mouse' || dragging || !shellRef.current) { + if (event.pointerType !== 'mouse' || !shellRef.current) { return; } @@ -1839,95 +2073,36 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) { setTilt({ x: 0, y: 0 }); }; - const handlePointerDown = (event: ReactPointerEvent) => { - if ( - event.pointerType !== 'mouse' || - event.button !== 0 || - !viewportRef.current - ) { + const handleSelectLabel = (label: string) => { + setActiveLabel(label); + + const containingFolderId = findContainingFolderId(visual.workspaceNav, label); + + if (!containingFolderId) { return; } - dragRef.current = { - active: true, - pointerId: event.pointerId, - startScrollLeft: viewportRef.current.scrollLeft, - startX: event.clientX, - }; - - viewportRef.current.setPointerCapture(event.pointerId); - setDragging(true); - event.preventDefault(); + setOpenFolderIds((current) => + current.includes(containingFolderId) + ? current + : [...current, containingFolderId], + ); }; - const handlePointerMove = (event: ReactPointerEvent) => { - if (!dragRef.current.active || !viewportRef.current) { - return; - } - - viewportRef.current.scrollLeft = - dragRef.current.startScrollLeft - - (event.clientX - dragRef.current.startX); + const handleToggleFolder = (folderId: string) => { + setOpenFolderIds((current) => + current.includes(folderId) + ? current.filter((id) => id !== folderId) + : [...current, folderId], + ); }; - const endDragging = () => { - dragRef.current.active = false; - dragRef.current.pointerId = -1; - setDragging(false); - }; - - const handlePointerUp = (event: ReactPointerEvent) => { - if (!viewportRef.current || dragRef.current.pointerId !== event.pointerId) { - return; - } - - viewportRef.current.releasePointerCapture(event.pointerId); - endDragging(); - }; - - useEffect(() => { - const node = viewportRef.current; - - if (!node) { - return; - } - - const onWheel: EventListener = (event) => { - if (!(event instanceof WheelEvent)) { - return; - } - - if ( - Math.abs(event.deltaY) <= Math.abs(event.deltaX) - ) { - return; - } - - const maxScrollLeft = Math.max(node.scrollWidth - node.clientWidth, 0); - const nextScrollLeft = Math.min( - Math.max(node.scrollLeft + event.deltaY, 0), - maxScrollLeft, - ); - - if (Math.abs(nextScrollLeft - node.scrollLeft) < 0.5) { - return; - } - - node.scrollLeft = nextScrollLeft; - event.preventDefault(); - }; - - node.addEventListener('wheel', onWheel, { passive: false }); - - return () => { - node.removeEventListener('wheel', onWheel); - }; - }, []); - const renderSidebarEntry = (entry: HeroSidebarEntry) => { if (isFolder(entry)) { return ( handleToggleFolder(entry.id)} selectedLabel={activeLabel} /> ); @@ -1946,7 +2122,7 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) { ); @@ -2009,14 +2185,17 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) { {visual.favoritesNav.map((item) => ( ))} ) : null} - Workspace + + Workspace + {visual.workspaceNav.map(renderSidebarEntry)} @@ -2026,194 +2205,89 @@ export function HomeVisual({ visual }: { visual: HeroVisualType }) { - - - + {activeItem ? renderSidebarIcon(activeItem.icon) : null} {activeLabel} - - - - - - New Record - - - - - - - - - ⌘K - - + {navbarActions ? ( + navbarActions.map(renderNavbarAction) + ) : ( + <> + + + + + + New Record + + + + + + + + + ⌘K + + + + )} - - - - {actions.map((action) => ( - {action} - ))} - - - - - - - - - - {columns.map((column) => ( - - - {column.isFirstColumn ? ( - <> - - - - {renderHeaderIcon(column.id)} - {column.label} - - - ) : ( - <> - {renderHeaderIcon(column.id)} - {column.label} - - )} - - - ))} - - {fillerWidth > 0 ? ( - - - + {showViewBar ? ( + + - - - {rows.map((row) => { - const hovered = hoveredRowId === row.id; - - return ( - setHoveredRowId(row.id)} - onMouseLeave={() => - setHoveredRowId((current) => - current === row.id ? null : current, - ) - } - > - {columns.map((column) => { - const cell = row.cells[column.id]; - - return ( - - {cell - ? renderCellValue( - cell, - hovered, - !!column.isFirstColumn, - column.id, - ) - : null} - - ); - })} - - - ); - })} - - - {columns.length > 0 ? ( - - - Calculate - - - - ) : null} - {columns.slice(1).map((column) => ( - + + ) : ( + + {activeHeader?.title ?? activeLabel} + + )} + + {activeActions.length > 0 ? ( + + {activeActions.map((action) => ( + {action} ))} - - - - - + + ) : null} + + ) : null} + + {activePage + ? renderPageDefinition(activePage, handleSelectLabel) + : null} diff --git a/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/HomeVisualLoader.tsx b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/HomeVisualLoader.tsx new file mode 100644 index 0000000000..e378b6546e --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/HomeVisualLoader.tsx @@ -0,0 +1,51 @@ +import { styled } from '@linaria/react'; +import { motion } from 'framer-motion'; +import { VISUAL_TOKENS } from './homeVisualTokens'; + +const DEFAULT_LOADER_COLOR = `var(--tw-button-color, ${VISUAL_TOKENS.font.color.tertiary})`; + +const StyledLoaderContainer = styled.div` + align-items: center; + border: 1px solid ${DEFAULT_LOADER_COLOR}; + border-radius: ${VISUAL_TOKENS.border.radius.pill}; + box-sizing: border-box; + display: flex; + gap: ${VISUAL_TOKENS.spacing[2]}; + height: 12px; + justify-content: center; + overflow: hidden; + width: 24px; +`; + +const StyledLoader = styled(motion.div)` + background-color: ${DEFAULT_LOADER_COLOR}; + border-radius: ${VISUAL_TOKENS.border.radius.pill}; + height: 8px; + width: 8px; +`; + +type HomeVisualLoaderProps = { + color?: string; +}; + +export function HomeVisualLoader({ color }: HomeVisualLoaderProps) { + return ( + + + + ); +} diff --git a/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/KanbanPage.tsx b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/KanbanPage.tsx new file mode 100644 index 0000000000..df285338b3 --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/KanbanPage.tsx @@ -0,0 +1,638 @@ +'use client'; + +import { theme } from '@/theme'; +import { styled } from '@linaria/react'; +import { + IconBuildingSkyscraper, + IconCalendarEvent, + IconCheck, + IconCurrencyDollar, + IconId, + IconPlus, + IconStar, + IconUser, + IconUserCircle, +} from '@tabler/icons-react'; +import { useState } from 'react'; +import type { + HeroCellEntity, + HeroCellPerson, + HeroKanbanCardType, + HeroKanbanLaneType, + HeroKanbanPageDefinition, +} from '../../types/HeroHomeData'; +import { Chip } from './homeVisualChip'; +import { VISUAL_TOKENS } from './homeVisualTokens'; + +const APP_FONT = VISUAL_TOKENS.font.family; +const TABLER_STROKE = 1.6; +const LANE_WIDTH = 206.4; + +const COLORS = { + accentBorder: VISUAL_TOKENS.border.color.blue, + accentSurfaceSoft: VISUAL_TOKENS.background.transparent.blue, + background: VISUAL_TOKENS.background.primary, + backgroundSecondary: VISUAL_TOKENS.background.secondary, + border: VISUAL_TOKENS.border.color.medium, + borderLight: VISUAL_TOKENS.border.color.light, + borderStrong: VISUAL_TOKENS.border.color.strong, + shadow: VISUAL_TOKENS.boxShadow.light, + text: VISUAL_TOKENS.font.color.primary, + textLight: VISUAL_TOKENS.font.color.light, + textSecondary: VISUAL_TOKENS.font.color.secondary, + textTertiary: VISUAL_TOKENS.font.color.tertiary, +}; + +const PERSON_TONES: Record = { + amber: { background: '#f6e6d7', color: '#7a4f2a' }, + blue: { background: '#dbeafe', color: '#1d4ed8' }, + gray: { background: '#e5e7eb', color: '#4b5563' }, + green: { background: '#dcfce7', color: '#15803d' }, + pink: { background: '#ffe4e6', color: '#be123c' }, + purple: { background: '#ede9fe', color: '#6d28d9' }, + red: { background: '#fee2e2', color: '#b91c1c' }, + teal: { background: '#ccfbf1', color: '#0f766e' }, +}; + +const LANE_TONES: Record = { + blue: { background: '#def4ff', color: '#007bb8' }, + gray: { background: '#f3f1ef', color: '#666666' }, + green: { background: '#dcf7ed', color: '#1a7f50' }, + pink: { background: '#fce5f3', color: '#d6409f' }, + purple: { background: '#ede9fe', color: '#8e4ec6' }, +}; + +const failedAvatarUrls = new Set(); +const failedFaviconUrls = new Set(); + +const BoardShell = styled.div` + flex: 1 1 auto; + min-height: 0; + min-width: 0; + overflow: auto; + scrollbar-width: none; + width: 100%; + + &::-webkit-scrollbar { + display: none; + } +`; + +const BoardCanvas = styled.div<{ $laneCount: number }>` + box-sizing: border-box; + display: grid; + grid-template-columns: repeat( + ${({ $laneCount }) => $laneCount}, + minmax(${LANE_WIDTH}px, 1fr) + ); + min-height: 100%; + min-width: ${({ $laneCount }) => + `max(100%, ${$laneCount * LANE_WIDTH + 16}px)`}; + padding: 0 8px; + width: 100%; +`; + +const Lane = styled.div<{ $last?: boolean }>` + border-right: ${({ $last }) => + $last ? 'none' : `1px solid ${COLORS.borderLight}`}; + display: flex; + flex-direction: column; + min-height: 0; + min-width: 0; +`; + +const LaneHeader = styled.div` + align-items: center; + display: flex; + gap: 4px; + min-height: 40px; + padding: 8px; +`; + +const LaneTag = styled.span<{ $background: string; $color: string }>` + align-items: center; + background: ${({ $background }) => $background}; + border-radius: 4px; + color: ${({ $color }) => $color}; + display: inline-flex; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: ${theme.font.weight.medium}; + height: 20px; + line-height: 1.4; + padding: 0 8px; + white-space: nowrap; +`; + +const LaneCount = styled.span` + color: ${COLORS.textTertiary}; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: ${theme.font.weight.regular}; + line-height: 1.4; + white-space: nowrap; +`; + +const LaneBody = styled.div` + display: flex; + flex: 1 1 auto; + flex-direction: column; + gap: 8px; + min-height: 0; + padding: 0 8px 8px; +`; + +const Card = styled.div` + background: ${COLORS.backgroundSecondary}; + border: 1px solid ${COLORS.border}; + border-radius: 4px; + box-shadow: ${COLORS.shadow}; + display: flex; + flex-direction: column; + overflow: hidden; +`; + +const CardHeader = styled.div` + align-items: center; + display: flex; + gap: 8px; + justify-content: space-between; + padding: 8px 8px 4px; +`; + +const CardTitle = styled.span` + color: ${COLORS.text}; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: ${theme.font.weight.medium}; + line-height: 1.4; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const CheckboxContainer = styled.div` + align-items: center; + display: flex; + flex: 0 0 24px; + height: 24px; + justify-content: center; + width: 24px; +`; + +const CheckboxBox = styled.div<{ $checked?: boolean }>` + align-items: center; + background: ${({ $checked }) => + $checked ? COLORS.accentSurfaceSoft : 'transparent'}; + border: 1px solid + ${({ $checked }) => ($checked ? COLORS.accentBorder : COLORS.borderStrong)}; + border-radius: 3px; + color: ${COLORS.textSecondary}; + display: flex; + height: 14px; + justify-content: center; + width: 14px; +`; + +const CardFields = styled.div` + display: flex; + flex-direction: column; + gap: 2px; + padding: 0 8px 4px 10px; +`; + +const FieldRow = styled.div` + align-items: center; + display: flex; + gap: 4px; + min-height: 24px; + width: 100%; +`; + +const FieldIcon = styled.div` + align-items: center; + color: ${COLORS.textTertiary}; + display: flex; + flex: 0 0 16px; + height: 16px; + justify-content: center; + width: 16px; +`; + +const FieldValueWrap = styled.div` + align-items: center; + display: flex; + flex: 1 1 auto; + min-width: 0; + overflow: hidden; +`; + +const FieldText = styled.span` + color: ${COLORS.text}; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: ${theme.font.weight.regular}; + line-height: 1.4; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const StarsRow = styled.div` + align-items: center; + display: inline-flex; + gap: 2px; + padding: 0 4px; +`; + +const StarGlyph = styled.span<{ $filled: boolean }>` + color: ${({ $filled }) => ($filled ? COLORS.textSecondary : '#d6d6d6')}; + font-family: ${APP_FONT}; + font-size: 13px; + line-height: 1; +`; + +const AddCardButton = styled.div` + align-items: center; + color: ${COLORS.textTertiary}; + display: inline-flex; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: ${theme.font.weight.regular}; + gap: 4px; + height: 24px; + line-height: 1.4; + padding: 0 4px; + white-space: nowrap; +`; + +const PersonAvatarCircle = styled.div<{ + $background: string; + $color: string; + $square?: boolean; +}>` + align-items: center; + background: ${({ $background }) => $background}; + border-radius: ${({ $square }) => ($square ? '4px' : '999px')}; + color: ${({ $color }) => $color}; + display: flex; + flex: 0 0 auto; + font-family: ${APP_FONT}; + font-size: 10px; + font-weight: ${theme.font.weight.medium}; + height: 14px; + justify-content: center; + overflow: hidden; + width: 14px; +`; + +const AvatarImage = styled.img` + display: block; + height: 100%; + object-fit: cover; + width: 100%; +`; + +const FaviconImage = styled.img` + display: block; + height: 100%; + object-fit: contain; + width: 100%; +`; + +function getInitials(value: string) { + return value + .split(' ') + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]) + .join('') + .toUpperCase(); +} + +function sanitizeURL(link: string | null | undefined) { + return link + ? link.replace(/(https?:\/\/)|(www\.)/g, '').replace(/\/$/, '') + : ''; +} + +function getLogoUrlFromDomainName(domainName?: string): string | undefined { + const sanitizedDomain = sanitizeURL(domainName); + + return sanitizedDomain + ? `https://twenty-icons.com/${sanitizedDomain}` + : undefined; +} + +function FaviconLogo({ + src, + domain, + label, + size = 14, +}: { + domain?: string; + label?: string; + size?: number; + src?: string; +}) { + const faviconUrl = src ?? getLogoUrlFromDomainName(domain); + const [localFailedUrl, setLocalFailedUrl] = useState(null); + const showFavicon = + faviconUrl !== undefined && + !failedFaviconUrls.has(faviconUrl) && + localFailedUrl !== faviconUrl; + + const baseStyle = { + alignItems: 'center', + borderRadius: '4px', + display: 'flex', + flex: '0 0 auto', + fontFamily: APP_FONT, + fontSize: size <= 14 ? '8px' : '9px', + fontWeight: 600, + height: `${size}px`, + justifyContent: 'center', + lineHeight: 1, + overflow: 'hidden', + width: `${size}px`, + } as const; + + if (showFavicon) { + return ( +
+ { + failedFaviconUrls.add(faviconUrl); + setLocalFailedUrl(faviconUrl); + }} + /> +
+ ); + } + + const initials = label ? getInitials(label) : '?'; + + return ( +
+ {initials.slice(0, 1)} +
+ ); +} + +function PersonAvatarContent({ token }: { token: HeroCellPerson }) { + const [localFailedUrl, setLocalFailedUrl] = useState(null); + const showAvatar = + token.avatarUrl !== undefined && + !failedAvatarUrls.has(token.avatarUrl) && + localFailedUrl !== token.avatarUrl; + + if (showAvatar) { + return ( + { + if (token.avatarUrl) { + failedAvatarUrls.add(token.avatarUrl); + setLocalFailedUrl(token.avatarUrl); + } + }} + /> + ); + } + + return token.shortLabel ?? getInitials(token.name); +} + +function EntityChip({ entity }: { entity: HeroCellEntity }) { + return ( + } + maxWidth={152} + /> + ); +} + +function PersonChip({ person }: { person: HeroCellPerson }) { + const tone = PERSON_TONES[person.tone ?? 'gray'] ?? PERSON_TONES.gray; + const square = + person.kind === 'api' || + person.kind === 'system' || + person.kind === 'workflow'; + + return ( + + +
+ } + maxWidth={152} + /> + ); +} + +function RatingValue({ rating }: { rating: number }) { + return ( + + {Array.from({ length: 5 }, (_, index) => ( + + {index < rating ? '★' : '★'} + + ))} + + ); +} + +function Checkbox({ checked = false }: { checked?: boolean }) { + return ( + + + {checked ? ( + + ) : null} + + + ); +} + +function KanbanCard({ card }: { card: HeroKanbanCardType }) { + return ( + + + {card.title} + + + + + + + + + + {card.amount} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {card.date} + + + + + + + + + + + + + + + + + + {card.recordId} + + + + + ); +} + +function KanbanLane({ + lane, + isLast, +}: { + isLast: boolean; + lane: HeroKanbanLaneType; +}) { + const tone = LANE_TONES[lane.tone] ?? LANE_TONES.gray; + + return ( + + + + {lane.label} + + {lane.cards.length} + + + + {lane.cards.map((card) => ( + + ))} + + + + + ); +} + +export function KanbanPage({ page }: { page: HeroKanbanPageDefinition }) { + return ( + + + {page.lanes.map((lane, index) => ( + + ))} + + + ); +} diff --git a/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/PagePreviewLoader.tsx b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/PagePreviewLoader.tsx new file mode 100644 index 0000000000..8871fce9f3 --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/PagePreviewLoader.tsx @@ -0,0 +1,25 @@ +import { styled } from '@linaria/react'; +import { HomeVisualLoader } from './HomeVisualLoader'; + +const StyledPagePreviewLoader = styled.div` + align-items: center; + display: flex; + height: 100%; + justify-content: center; + min-height: 100%; + min-width: 0; + padding: 24px; + width: 100%; +`; + +type PagePreviewLoaderProps = { + ariaLabel: string; +}; + +export function PagePreviewLoader({ ariaLabel }: PagePreviewLoaderProps) { + return ( + + + + ); +} diff --git a/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/SalesDashboardPage.tsx b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/SalesDashboardPage.tsx new file mode 100644 index 0000000000..e7f51ba22f --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/SalesDashboardPage.tsx @@ -0,0 +1,150 @@ +import type { + HeroDashboardChartImageType, + HeroDashboardPageDefinition, +} from '@/sections/Hero/types'; +import { theme } from '@/theme'; +import { styled } from '@linaria/react'; +import { VISUAL_TOKENS } from './homeVisualTokens'; + +const APP_FONT = VISUAL_TOKENS.font.family; +const CARD_BACKGROUND = VISUAL_TOKENS.background.secondary; +const CARD_BORDER = VISUAL_TOKENS.border.color.light; + +const DashboardGrid = styled.div` + display: grid; + gap: 8px; + grid-template-areas: + 'metrics' + 'visits' + 'revenue' + 'distribution'; + grid-template-columns: minmax(0, 1fr); + min-width: 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 + ); + } +`; + +const MetricStack = styled.div` + align-self: stretch; + display: grid; + gap: 8px; + grid-area: metrics; + min-width: 0; + + @media (min-width: ${theme.breakpoints.md}px) { + grid-template-rows: repeat(3, minmax(0, 1fr)); + } +`; + +const MetricCard = styled.div` + background: ${CARD_BACKGROUND}; + border: 1px solid ${CARD_BORDER}; + border-radius: 8px; + display: flex; + flex-direction: column; + gap: 16px; + 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}; + 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-weight: 600; + line-height: 1.2; +`; + +const VisitsChart = styled.div` + grid-area: visits; + min-width: 0; +`; + +const RevenueChart = styled.div` + grid-area: revenue; + min-width: 0; +`; + +const DistributionChart = styled.div` + grid-area: distribution; + min-width: 0; +`; + +const ChartImage = styled.img` + display: block; + height: auto; + max-width: 100%; + width: 100%; +`; + +function DashboardChart({ + chart, + className, +}: { + chart: HeroDashboardChartImageType; + className?: string; +}) { + return ( + + ); +} + +export function SalesDashboardPage({ + page, +}: { + page: HeroDashboardPageDefinition; +}) { + return ( + + + {page.dashboard.metrics.map((metric) => ( + + {metric.title} + {metric.value} + + ))} + + + + + + + + + + + + + + + ); +} diff --git a/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/TablePage.tsx b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/TablePage.tsx new file mode 100644 index 0000000000..2ee4cd2e4e --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/TablePage.tsx @@ -0,0 +1,1057 @@ +'use client'; + +import { theme } from '@/theme'; +import { styled } from '@linaria/react'; +import { + IconBrandLinkedin, + IconBuildingFactory2, + IconCalendarEvent, + IconCheck, + IconChevronDown, + IconCopy, + IconCreativeCommonsSa, + IconLink, + IconMap2, + IconMoneybag, + IconPencil, + IconPlus, + IconTarget, + IconTargetArrow, + IconUser, + IconUserCircle, + IconUsers, + IconX, +} from '@tabler/icons-react'; +import { + useEffect, + useRef, + useState, + type PointerEvent as ReactPointerEvent, + type ReactNode, +} from 'react'; +import type { + HeroCellEntity, + HeroCellPerson, + HeroCellRelation, + HeroCellText, + HeroCellValue, + HeroTablePageDefinition, +} from '../../types/HeroHomeData'; +import { Chip, ChipVariant } from './homeVisualChip'; +import { VISUAL_TOKENS } from './homeVisualTokens'; + +const APP_FONT = VISUAL_TOKENS.font.family; +const TABLE_CELL_HORIZONTAL_PADDING = 8; +const HOVER_ACTION_EDGE_INSET = 4; +const TABLER_STROKE = 1.6; + +const COLORS = { + accentBorder: VISUAL_TOKENS.border.color.blue, + accentSurface: VISUAL_TOKENS.accent.primary, + accentSurfaceSoft: VISUAL_TOKENS.background.transparent.blue, + background: VISUAL_TOKENS.background.primary, + backgroundSecondary: VISUAL_TOKENS.background.secondary, + border: VISUAL_TOKENS.border.color.medium, + borderLight: VISUAL_TOKENS.border.color.light, + borderStrong: VISUAL_TOKENS.border.color.strong, + text: VISUAL_TOKENS.font.color.primary, + textSecondary: VISUAL_TOKENS.font.color.secondary, + textTertiary: VISUAL_TOKENS.font.color.tertiary, +}; + +const PERSON_TONES: Record = { + amber: { background: '#f6e6d7', color: '#7a4f2a' }, + blue: { background: '#dbeafe', color: '#1d4ed8' }, + gray: { background: '#e5e7eb', color: '#4b5563' }, + green: { background: '#dcfce7', color: '#15803d' }, + orange: { background: '#ffdcc3', color: '#ED5F00' }, + pink: { background: '#ffe4e6', color: '#be123c' }, + purple: { background: '#ede9fe', color: '#6d28d9' }, + red: { background: '#fee2e2', color: '#b91c1c' }, + teal: { background: '#ccfbf1', color: '#0f766e' }, +}; + +const ROW_HOVER_ACTION_DISABLED_COLUMNS = new Set([ + 'createdBy', + 'accountOwner', +]); + +const HEADER_ICON_MAP: Record = { + added: IconCalendarEvent, + accountOwner: IconUserCircle, + address: IconMap2, + arr: IconMoneybag, + createdBy: IconCreativeCommonsSa, + employees: IconUsers, + icp: IconTarget, + industry: IconBuildingFactory2, + linkedin: IconBrandLinkedin, + mainContact: IconUser, + opportunities: IconTargetArrow, + url: IconLink, +}; + +const failedAvatarUrls = new Set(); +const failedFaviconUrls = new Set(); + +type MiniIconProps = { + color?: string; + size?: number; +}; + +const TableShell = styled.div` + display: flex; + flex: 1 1 auto; + min-height: 0; + min-width: 0; + overflow: hidden; + width: 100%; +`; + +const GripRail = styled.div` + background: ${COLORS.background}; + display: grid; + flex: 0 0 12px; + grid-auto-rows: 32px; + width: 12px; +`; + +const GripCell = styled.div` + background: ${COLORS.background}; + border-bottom: 1px solid ${COLORS.borderLight}; +`; + +const TableViewport = styled.div<{ $dragging: boolean }>` + cursor: ${({ $dragging }) => ($dragging ? 'grabbing' : 'grab')}; + flex: 1 1 auto; + min-height: 0; + min-width: 0; + overflow-x: auto; + overflow-y: hidden; + overscroll-behavior-x: contain; + scrollbar-width: none; + width: 100%; + + &::-webkit-scrollbar { + display: none; + } +`; + +const TableCanvas = styled.div<{ $width: number }>` + display: flex; + flex-direction: column; + height: 100%; + min-height: 100%; + min-width: ${({ $width }) => `${$width}px`}; + width: ${({ $width }) => `${$width}px`}; +`; + +const HeaderRow = styled.div` + display: flex; +`; + +const DataRow = styled.div` + display: flex; +`; + +const FooterRow = styled.div` + display: flex; +`; + +const TableCell = styled.div<{ + $align?: 'left' | 'right'; + $header?: boolean; + $hovered?: boolean; + $sticky?: boolean; + $width: number; +}>` + align-items: center; + background: ${({ $header, $hovered }) => { + if ($header) { + return COLORS.background; + } + + return $hovered ? COLORS.backgroundSecondary : COLORS.background; + }}; + border-bottom: 1px solid ${COLORS.borderLight}; + border-right: 1px solid ${COLORS.borderLight}; + box-sizing: border-box; + display: flex; + flex: 0 0 ${({ $width }) => `${$width}px`}; + height: 32px; + justify-content: ${({ $align }) => + $align === 'right' ? 'flex-end' : 'flex-start'}; + left: ${({ $sticky }) => ($sticky ? '0' : 'auto')}; + min-width: ${({ $width }) => `${$width}px`}; + padding: 0 ${TABLE_CELL_HORIZONTAL_PADDING}px; + position: ${({ $sticky }) => ($sticky ? 'sticky' : 'relative')}; + z-index: ${({ $header, $sticky }) => { + if ($sticky && $header) { + return 6; + } + + if ($sticky) { + return 4; + } + + return 1; + }}; +`; + +const EmptyFillCell = styled.div<{ + $footer?: boolean; + $header?: boolean; + $hovered?: boolean; + $width: number; +}>` + background: ${({ $header, $hovered, $footer }) => { + if ($header || $footer) { + return COLORS.background; + } + + return $hovered ? COLORS.backgroundSecondary : COLORS.background; + }}; + border-bottom: 1px solid ${COLORS.borderLight}; + flex: 0 0 ${({ $width }) => `${$width}px`}; + min-width: ${({ $width }) => `${$width}px`}; +`; + +const HeaderCellContent = styled.div` + align-items: center; + display: flex; + gap: 4px; + height: 100%; + min-width: 0; + width: 100%; +`; + +const HeaderLabel = styled.span` + color: ${COLORS.textTertiary}; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: ${theme.font.weight.medium}; + line-height: 1.4; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const EdgePlus = styled.div` + margin-left: auto; +`; + +const CheckboxContainer = styled.div` + align-items: center; + display: flex; + flex: 0 0 24px; + height: 24px; + justify-content: center; + width: 24px; +`; + +const CheckboxBox = styled.div<{ $checked?: boolean }>` + align-items: center; + background: ${({ $checked }) => + $checked ? COLORS.accentSurfaceSoft : 'transparent'}; + border: 1px solid + ${({ $checked }) => ($checked ? COLORS.accentBorder : COLORS.borderStrong)}; + border-radius: 3px; + display: flex; + flex: 0 0 auto; + height: 14px; + justify-content: center; + width: 14px; +`; + +const EntityCellLayout = styled.div` + align-items: center; + display: flex; + gap: 4px; + height: 100%; + min-width: 0; + position: relative; + width: 100%; +`; + +const CellHoverAnchor = styled.div` + align-items: center; + display: flex; + height: 100%; + min-width: 0; + position: relative; + width: 100%; +`; + +const CellChip = styled(Chip)` + max-width: 100%; + min-width: 0; +`; + +const InlineText = styled.span` + color: ${COLORS.text}; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: ${theme.font.weight.regular}; + line-height: 1.4; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const MutedText = styled.span` + color: ${COLORS.textTertiary}; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: ${theme.font.weight.regular}; + line-height: 1.4; + white-space: nowrap; +`; + +const RightAlignedText = styled(InlineText)` + text-align: right; + width: 100%; +`; + +const PersonAvatarCircle = styled.div<{ + $background: string; + $color: string; + $square?: boolean; +}>` + align-items: center; + background: ${({ $background }) => $background}; + border-radius: ${({ $square }) => ($square ? '4px' : '999px')}; + color: ${({ $color }) => $color}; + display: flex; + flex: 0 0 auto; + font-family: ${APP_FONT}; + font-size: 10px; + font-weight: ${theme.font.weight.medium}; + height: 14px; + justify-content: center; + overflow: hidden; + width: 14px; +`; + +const AvatarImage = styled.img` + display: block; + height: 100%; + object-fit: cover; + width: 100%; +`; + +const BooleanRow = styled.div` + align-items: center; + display: inline-flex; + gap: 4px; +`; + +const HoverActions = styled.div<{ $rightInset?: number; $visible: boolean }>` + align-items: center; + background: ${VISUAL_TOKENS.background.transparent.primary}; + border: 1px solid ${VISUAL_TOKENS.background.transparent.light}; + border-radius: 4px; + bottom: 4px; + box-sizing: border-box; + box-shadow: ${VISUAL_TOKENS.boxShadow.light}; + display: flex; + gap: 0; + justify-content: center; + opacity: ${({ $visible }) => ($visible ? 1 : 0)}; + padding: 0 4px; + pointer-events: none; + position: absolute; + right: ${({ $rightInset = HOVER_ACTION_EDGE_INSET - TABLE_CELL_HORIZONTAL_PADDING }) => + `${$rightInset}px`}; + top: 4px; + transform: translateX(${({ $visible }) => ($visible ? '0' : '4px')}); + transition: + opacity 0.14s ease, + transform 0.14s ease; + width: 24px; +`; + +const MiniAction = styled.div` + align-items: center; + border-radius: 2px; + color: ${COLORS.textSecondary}; + display: flex; + height: 16px; + justify-content: center; + width: 16px; +`; + +const FooterFirstContent = styled.div` + align-items: center; + display: inline-flex; + gap: 4px; + padding-left: 28px; +`; + +const HeaderFillContent = styled.div` + align-items: center; + display: flex; + height: 100%; + padding: 0 8px; +`; + +const TagChip = styled.div` + align-items: center; + background: ${VISUAL_TOKENS.background.transparent.light}; + border-radius: 4px; + color: ${COLORS.textSecondary}; + display: inline-flex; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: ${theme.font.weight.regular}; + height: 20px; + line-height: 1.4; + max-width: 100%; + min-width: 0; + overflow: hidden; + padding: 0 8px; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const MultiChipStack = styled.div` + align-items: center; + display: flex; + gap: 4px; + min-width: 0; + overflow: hidden; + width: 100%; +`; + +const FaviconImage = styled.img` + display: block; + height: 100%; + object-fit: contain; + width: 100%; +`; + +function getInitials(value: string) { + return value + .split(' ') + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]) + .join('') + .toUpperCase(); +} + +function sanitizeURL(link: string | null | undefined) { + return link + ? link.replace(/(https?:\/\/)|(www\.)/g, '').replace(/\/$/, '') + : ''; +} + +function getLogoUrlFromDomainName(domainName?: string): string | undefined { + const sanitizedDomain = sanitizeURL(domainName); + + return sanitizedDomain + ? `https://twenty-icons.com/${sanitizedDomain}` + : undefined; +} + +function PlusMini({ color = COLORS.textSecondary, size = 14 }: MiniIconProps) { + return ( + + ); +} + +function CheckMini({ color = COLORS.text, size = 12 }: MiniIconProps) { + return ( + + ); +} + +function CloseMini({ color = COLORS.text, size = 12 }: MiniIconProps) { + return ; +} + +function PencilMini({ + color = COLORS.textSecondary, + size = 14, +}: MiniIconProps) { + return ( + + ); +} + +function CopyMini({ + color = COLORS.textSecondary, + size = 14, +}: MiniIconProps) { + return ( + + ); +} + +function ChevronDownMini({ + color = COLORS.textTertiary, + size = 14, +}: MiniIconProps) { + return ( + + ); +} + +function FaviconLogo({ + src, + domain, + label, + size = 14, +}: { + domain?: string; + label?: string; + size?: number; + src?: string; +}) { + const faviconUrl = src ?? getLogoUrlFromDomainName(domain); + const [localFailedUrl, setLocalFailedUrl] = useState(null); + const showFavicon = + faviconUrl !== undefined && + !failedFaviconUrls.has(faviconUrl) && + localFailedUrl !== faviconUrl; + + const baseStyle = { + width: `${size}px`, + height: `${size}px`, + borderRadius: '4px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flex: '0 0 auto', + overflow: 'hidden', + fontFamily: APP_FONT, + fontSize: size <= 14 ? '8px' : '9px', + fontWeight: 600, + lineHeight: 1, + } as const; + + if (showFavicon) { + return ( +
+ { + failedFaviconUrls.add(faviconUrl); + setLocalFailedUrl(faviconUrl); + }} + /> +
+ ); + } + + const initials = label ? getInitials(label) : '?'; + + return ( +
+ {initials.slice(0, 1)} +
+ ); +} + +function PersonAvatarContent({ token }: { token: HeroCellPerson }) { + const [localFailedUrl, setLocalFailedUrl] = useState(null); + const showAvatar = + token.avatarUrl !== undefined && + !failedAvatarUrls.has(token.avatarUrl) && + localFailedUrl !== token.avatarUrl; + + if (showAvatar) { + return ( + { + if (token.avatarUrl) { + failedAvatarUrls.add(token.avatarUrl); + setLocalFailedUrl(token.avatarUrl); + } + }} + /> + ); + } + + return token.shortLabel ?? getInitials(token.name); +} + +function PersonTokenCell({ + token, + hovered = false, + withCopyAction = true, +}: { + hovered?: boolean; + token: HeroCellPerson; + withCopyAction?: boolean; +}) { + const tone = PERSON_TONES[token.tone ?? 'gray'] ?? PERSON_TONES.gray; + const square = + token.kind === 'api' || + token.kind === 'system' || + token.kind === 'workflow'; + + return ( + + + + + } + /> + + {withCopyAction ? ( + + ) : null} + + + ); +} + +function EntityCellComponent({ + cell, + hovered, + isFirstColumn, +}: { + cell: HeroCellEntity; + hovered: boolean; + isFirstColumn: boolean; +}) { + if (isFirstColumn) { + return ( + + + + + } + variant={ChipVariant.Highlighted} + /> + + + + + ); + } + + return ( + } + /> + ); +} + +function RelationCellComponent({ + cell, + hovered, +}: { + cell: HeroCellRelation; + hovered: boolean; +}) { + return ( + + + {cell.items.map((item) => { + const tone = PERSON_TONES[item.tone ?? 'gray'] ?? PERSON_TONES.gray; + + return ( + + {item.shortLabel ?? getInitials(item.name)} + + } + /> + ); + })} + + + + + + ); +} + +function TextCellComponent({ + cell, + isFirstColumn, + onNavigateToLabel, +}: { + cell: HeroCellText; + isFirstColumn: boolean; + onNavigateToLabel?: (label: string) => void; +}) { + const targetLabel = cell.targetLabel; + const handleNavigate = + targetLabel && onNavigateToLabel + ? () => onNavigateToLabel(targetLabel) + : undefined; + + if (!isFirstColumn || !cell.shortLabel) { + return {cell.value}; + } + + const tone = PERSON_TONES[cell.tone ?? 'gray'] ?? PERSON_TONES.gray; + + return ( + + {cell.shortLabel} + + } + onClick={handleNavigate} + /> + ); +} + +function renderCellValue( + cell: HeroCellValue, + hovered: boolean, + isFirstColumn: boolean, + columnId: string, + onNavigateToLabel?: (label: string) => void, +): ReactNode { + const showHoverAction = !ROW_HOVER_ACTION_DISABLED_COLUMNS.has(columnId); + + switch (cell.type) { + case 'text': + return ( + + ); + case 'number': + return {cell.value}; + case 'link': + return ( +
+ +
+ ); + case 'boolean': + return ( + + {cell.value ? : } + {cell.value ? 'True' : 'False'} + + ); + case 'tag': + return {cell.value}; + case 'person': + return ( + + ); + case 'entity': + return ( + + ); + case 'relation': + return ( + + ); + } +} + +function renderHeaderIcon(columnId: string): ReactNode { + const Icon = HEADER_ICON_MAP[columnId]; + + if (Icon) { + return ( + + ); + } + + return ( + + ); +} + +export function TablePage({ + page, + onNavigateToLabel, +}: { + page: HeroTablePageDefinition; + onNavigateToLabel?: (label: string) => void; +}) { + const viewportRef = useRef(null); + const dragRef = useRef({ + active: false, + pointerId: -1, + startScrollLeft: 0, + startX: 0, + }); + const [dragging, setDragging] = useState(false); + const [hoveredRowId, setHoveredRowId] = useState(null); + + const columnWidth = page.columns.reduce((sum, column) => sum + column.width, 0); + const totalTableWidth = page.width ?? columnWidth; + const fillerWidth = Math.max(totalTableWidth - columnWidth, 0); + + const handlePointerDown = (event: ReactPointerEvent) => { + if ( + event.pointerType !== 'mouse' || + event.button !== 0 || + !viewportRef.current + ) { + return; + } + + dragRef.current = { + active: true, + pointerId: event.pointerId, + startScrollLeft: viewportRef.current.scrollLeft, + startX: event.clientX, + }; + + viewportRef.current.setPointerCapture(event.pointerId); + setDragging(true); + event.preventDefault(); + }; + + const handlePointerMove = (event: ReactPointerEvent) => { + if (!dragRef.current.active || !viewportRef.current) { + return; + } + + viewportRef.current.scrollLeft = + dragRef.current.startScrollLeft - + (event.clientX - dragRef.current.startX); + }; + + const endDragging = () => { + dragRef.current.active = false; + dragRef.current.pointerId = -1; + setDragging(false); + }; + + const handlePointerUp = (event: ReactPointerEvent) => { + if (!viewportRef.current || dragRef.current.pointerId !== event.pointerId) { + return; + } + + viewportRef.current.releasePointerCapture(event.pointerId); + endDragging(); + }; + + useEffect(() => { + const node = viewportRef.current; + + if (!node) { + return; + } + + const onWheel: EventListener = (event) => { + if (!(event instanceof WheelEvent)) { + return; + } + + if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) { + return; + } + + const maxScrollLeft = Math.max(node.scrollWidth - node.clientWidth, 0); + const nextScrollLeft = Math.min( + Math.max(node.scrollLeft + event.deltaY, 0), + maxScrollLeft, + ); + + if (Math.abs(nextScrollLeft - node.scrollLeft) < 0.5) { + return; + } + + node.scrollLeft = nextScrollLeft; + event.preventDefault(); + }; + + node.addEventListener('wheel', onWheel, { passive: false }); + + return () => { + node.removeEventListener('wheel', onWheel); + }; + }, []); + + return ( + + + + + + + {page.columns.map((column) => ( + + + {column.isFirstColumn ? ( + <> + + + + {renderHeaderIcon(column.id)} + {column.label} + + + ) : ( + <> + {renderHeaderIcon(column.id)} + {column.label} + + )} + + + ))} + + {fillerWidth > 0 ? ( + + + + ) : null} + + + + {page.rows.map((row) => { + const hovered = hoveredRowId === row.id; + + return ( + setHoveredRowId(row.id)} + onMouseLeave={() => + setHoveredRowId((current) => + current === row.id ? null : current, + ) + } + > + {page.columns.map((column) => { + const cell = row.cells[column.id]; + + return ( + + {cell + ? renderCellValue( + cell, + hovered, + !!column.isFirstColumn, + column.id, + onNavigateToLabel, + ) + : null} + + ); + })} + + + ); + })} + + + {page.columns.length > 0 ? ( + + + Calculate + + + + ) : null} + {page.columns.slice(1).map((column) => ( + + ))} + + + + + + ); +} diff --git a/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/WorkflowPage.tsx b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/WorkflowPage.tsx new file mode 100644 index 0000000000..5ec1e00605 --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/WorkflowPage.tsx @@ -0,0 +1,495 @@ +'use client'; + +import type { HeroWorkflowPageDefinition } from '@/sections/Hero/types'; +import { theme } from '@/theme'; +import { styled } from '@linaria/react'; +import { + IconCode, + IconFilter, + IconPlug, + IconPlus, + IconRepeat, + IconSearch, + IconSitemap, +} from '@tabler/icons-react'; +import { VISUAL_TOKENS } from './homeVisualTokens'; + +const APP_FONT = VISUAL_TOKENS.font.family; +const TABLER_STROKE = 1.6; +const CANVAS_WIDTH = 1480; +const CANVAS_HEIGHT = 1260; +const NODE_HEIGHT = 48; +const CANVAS_TOP_OFFSET = 16; + +type WorkflowNodeDefinition = { + Icon: typeof IconPlug; + id: string; + iconColor: string; + label: 'Trigger' | 'Action'; + title: string; + width: number; + x: number; + y: number; +}; + +type WorkflowBranchLabel = { + text: string; + x: number; + y: number; +}; + +type WorkflowEdgeDefinition = { + from: string; + to: string; + type: 'branch' | 'curve' | 'vertical'; +}; + +const workflowNodes: WorkflowNodeDefinition[] = [ + { + id: 'trigger', + x: 370, + y: 80, + width: 238, + label: 'Trigger', + title: 'Record is created or updated', + Icon: IconPlug, + iconColor: '#4A67F6', + }, + { + id: 'is-personal-email', + x: 620, + y: 210, + width: 220, + label: 'Action', + title: 'Is this a personal email?', + Icon: IconCode, + iconColor: '#FF6B5F', + }, + { + id: 'if-business-email', + x: 640, + y: 340, + width: 180, + label: 'Action', + title: 'If business email', + Icon: IconFilter, + iconColor: VISUAL_TOKENS.font.color.secondary, + }, + { + id: 'extract-domain', + x: 620, + y: 470, + width: 220, + label: 'Action', + title: 'Extract domain from email', + Icon: IconCode, + iconColor: '#FF6B5F', + }, + { + id: 'search-company', + x: 640, + y: 600, + width: 180, + label: 'Action', + title: 'Search Company', + Icon: IconSearch, + iconColor: VISUAL_TOKENS.font.color.secondary, + }, + { + id: 'find-exact-match', + x: 610, + y: 730, + width: 240, + label: 'Action', + title: 'Find exact company match', + Icon: IconCode, + iconColor: '#FF6B5F', + }, + { + id: 'company-already-exists', + x: 600, + y: 860, + width: 260, + label: 'Action', + title: 'If a company already exists', + Icon: IconSitemap, + iconColor: VISUAL_TOKENS.font.color.secondary, + }, + { + id: 'attach-existing-company', + x: 370, + y: 990, + width: 240, + label: 'Action', + title: 'Attach person to existing company', + Icon: IconRepeat, + iconColor: VISUAL_TOKENS.font.color.secondary, + }, + { + id: 'create-company', + x: 840, + y: 990, + width: 220, + label: 'Action', + title: 'Create a new company', + Icon: IconPlus, + iconColor: VISUAL_TOKENS.font.color.secondary, + }, + { + id: 'attach-created-company', + x: 850, + y: 1120, + width: 240, + label: 'Action', + title: 'Attach person to this company', + Icon: IconRepeat, + iconColor: VISUAL_TOKENS.font.color.secondary, + }, +]; + +const workflowEdges: WorkflowEdgeDefinition[] = [ + { + from: 'trigger', + to: 'is-personal-email', + type: 'curve', + }, + { + from: 'is-personal-email', + to: 'if-business-email', + type: 'vertical', + }, + { + from: 'if-business-email', + to: 'extract-domain', + type: 'vertical', + }, + { + from: 'extract-domain', + to: 'search-company', + type: 'vertical', + }, + { + from: 'search-company', + to: 'find-exact-match', + type: 'vertical', + }, + { + from: 'find-exact-match', + to: 'company-already-exists', + type: 'vertical', + }, + { + from: 'company-already-exists', + to: 'attach-existing-company', + type: 'branch', + }, + { + from: 'company-already-exists', + to: 'create-company', + type: 'branch', + }, + { + from: 'create-company', + to: 'attach-created-company', + type: 'vertical', + }, +]; + +const workflowBranchLabels: WorkflowBranchLabel[] = [ + { x: 566, y: 944, text: 'if' }, + { x: 820, y: 944, text: 'else' }, +]; + +const PageShell = styled.div` + background: #ffffff; + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 100%; + min-width: 100%; +`; + +const CanvasViewportShell = styled.div` + background: #ffffff; + display: flex; + flex: 1 1 auto; + min-height: 0; + min-width: 0; + position: relative; +`; + +const CanvasViewport = styled.div` + display: flex; + flex: 1 1 auto; + min-height: 0; + min-width: 0; + overflow: auto; +`; + +const Canvas = styled.div` + background-color: #ffffff; + background-image: radial-gradient(circle, #ebebeb 1px, transparent 1.2px); + background-position: 10px 10px; + background-size: 20px 20px; + box-sizing: border-box; + height: ${CANVAS_HEIGHT + CANVAS_TOP_OFFSET}px; + min-height: 100%; + min-width: 100%; + overflow: hidden; + position: relative; + width: ${CANVAS_WIDTH}px; +`; + +const CanvasContent = styled.div` + height: ${CANVAS_HEIGHT}px; + left: calc((100% - ${CANVAS_WIDTH}px) / 2); + position: absolute; + top: ${CANVAS_TOP_OFFSET}px; + width: ${CANVAS_WIDTH}px; +`; + +const CanvasOverlay = styled.svg` + inset: 0; + overflow: visible; + pointer-events: none; + position: absolute; +`; + +const ActiveBadge = styled.div` + left: 8px; + position: absolute; + pointer-events: none; + top: 8px; + z-index: 3; +`; + +const ActiveBadgeLabel = styled.span` + align-items: center; + background: #dff3e6; + border-radius: 4px; + color: #228b52; + display: inline-flex; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: ${theme.font.weight.regular}; + height: 20px; + line-height: 1.4; + padding: 0 8px; +`; + +const Node = styled.div` + align-items: center; + background: ${VISUAL_TOKENS.background.secondary}; + border: 1px solid ${VISUAL_TOKENS.border.color.strong}; + border-radius: 8px; + box-sizing: border-box; + display: flex; + gap: 8px; + height: ${NODE_HEIGHT}px; + left: 0; + padding: 8px; + position: absolute; + top: 0; + z-index: 1; +`; + +const NodeIconContainer = styled.div` + align-items: center; + background: ${VISUAL_TOKENS.background.transparent.light}; + border-radius: 4px; + display: flex; + flex: 0 0 auto; + height: 32px; + justify-content: center; + width: 32px; +`; + +const NodeContent = styled.div` + align-items: stretch; + align-self: stretch; + display: flex; + flex: 1 1 auto; + flex-direction: column; + justify-content: space-between; + max-width: 184px; + min-width: 0; + padding-bottom: 2px; +`; + +const NodeLabel = styled.div` + color: ${VISUAL_TOKENS.font.color.tertiary}; + font-family: ${APP_FONT}; + font-size: 11px; + font-weight: 600; + line-height: 1; +`; + +const NodeTitle = styled.div` + color: ${VISUAL_TOKENS.font.color.primary}; + font-family: ${APP_FONT}; + font-size: 13px; + font-weight: ${theme.font.weight.medium}; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const BranchLabel = styled.div` + align-items: center; + background: ${VISUAL_TOKENS.background.secondary}; + border: 1px solid ${VISUAL_TOKENS.border.color.medium}; + border-radius: 6px; + color: ${VISUAL_TOKENS.font.color.tertiary}; + display: inline-flex; + font-family: ${APP_FONT}; + font-size: 11px; + font-weight: ${theme.font.weight.medium}; + height: 22px; + justify-content: center; + min-width: 24px; + padding: 0 6px; + position: absolute; + z-index: 2; +`; + +function getNodeById(nodeId: string) { + const node = workflowNodes.find((workflowNode) => workflowNode.id === nodeId); + + if (!node) { + throw new Error(`Unknown workflow node: ${nodeId}`); + } + + return node; +} + +function getNodeTopCenter(node: WorkflowNodeDefinition) { + return { + x: node.x + node.width / 2, + y: node.y, + }; +} + +function getNodeBottomCenter(node: WorkflowNodeDefinition) { + return { + x: node.x + node.width / 2, + y: node.y + NODE_HEIGHT + 1, + }; +} + +function getWorkflowEdgePath(edge: WorkflowEdgeDefinition) { + const fromNode = getNodeById(edge.from); + const toNode = getNodeById(edge.to); + const start = getNodeBottomCenter(fromNode); + const end = getNodeTopCenter(toNode); + + if (edge.type === 'vertical') { + return `M${start.x} ${start.y} L${end.x} ${end.y}`; + } + + if (edge.type === 'curve') { + const controlStartY = start.y + 28; + const controlEndY = end.y - 28; + + return `M${start.x} ${start.y} C${start.x} ${controlStartY} ${end.x} ${controlEndY} ${end.x} ${end.y}`; + } + + const controlStartY = start.y + 28; + const controlEndY = end.y - 28; + + return `M${start.x} ${start.y} C${start.x} ${controlStartY} ${end.x} ${controlEndY} ${end.x} ${end.y}`; +} + +function WorkflowNode({ + x, + y, + width, + label, + title, + Icon, + iconColor, +}: WorkflowNodeDefinition) { + return ( + + + + + + {label} + {title} + + + ); +} + +export function WorkflowPage({ page }: { page: HeroWorkflowPageDefinition }) { + return ( + + + + Active + + + + + + + + + + + {workflowEdges.map((edge) => ( + + ))} + + + {workflowNodes.map((node) => ( + + ))} + + {workflowBranchLabels.map((label) => ( + + {label.text} + + ))} + + + + + + ); +} diff --git a/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/homeVisualChip.tsx b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/homeVisualChip.tsx index 8d9fbd0e46..58c73df227 100644 --- a/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/homeVisualChip.tsx +++ b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/homeVisualChip.tsx @@ -1,5 +1,10 @@ import { styled } from '@linaria/react'; -import { type ReactNode } from 'react'; +import { + type KeyboardEvent, + type MouseEvent, + type PointerEvent, + type ReactNode, +} from 'react'; import { VISUAL_TOKENS } from './homeVisualTokens'; @@ -18,6 +23,7 @@ export type ChipProps = { leftComponent?: ReactNode | null; className?: string; maxWidth?: number; + onClick?: () => void; variant?: ChipVariant; }; @@ -126,15 +132,51 @@ export const Chip = ({ leftComponent = null, className, maxWidth, + onClick, variant = ChipVariant.Regular, }: ChipProps) => { + const isInteractive = clickable || onClick !== undefined; + + const handlePointerDown = (event: PointerEvent) => { + if (!isInteractive) { + return; + } + + event.stopPropagation(); + }; + + const handleClick = (event: MouseEvent) => { + if (!isInteractive) { + return; + } + + event.stopPropagation(); + onClick?.(); + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (!onClick) { + return; + } + + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + onClick(); + } + }; + return ( {leftComponent} {label} diff --git a/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/normalizeHeroPage.ts b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/normalizeHeroPage.ts new file mode 100644 index 0000000000..62eb804c41 --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/HomeVisual/normalizeHeroPage.ts @@ -0,0 +1,94 @@ +import type { + HeroKanbanPageDefinition, + HeroPageDefinition, + HeroSidebarItem, + HeroTablePageDefinition, + HeroWorkflowPageDefinition, +} from '../../types/HeroHomeData'; + +export type HeroPageDefaults = { + defaultActions: string[]; + defaultTableWidth?: number; +}; + +function normalizeTablePage( + page: HeroTablePageDefinition, + defaults: HeroPageDefaults, +): HeroTablePageDefinition { + return { + ...page, + header: { + ...page.header, + actions: page.header.actions ?? defaults.defaultActions, + count: page.header.count ?? page.rows.length, + showListIcon: page.header.showListIcon ?? true, + }, + width: page.width ?? defaults.defaultTableWidth, + }; +} + +function normalizeKanbanPage( + page: HeroKanbanPageDefinition, + defaults: HeroPageDefaults, +): HeroKanbanPageDefinition { + return { + ...page, + header: { + ...page.header, + actions: page.header.actions ?? defaults.defaultActions, + count: + page.header.count ?? + page.lanes.reduce((sum, lane) => sum + lane.cards.length, 0), + showListIcon: page.header.showListIcon ?? true, + }, + }; +} + +function normalizeExplicitPage( + page: HeroPageDefinition, + defaults: HeroPageDefaults, +): HeroPageDefinition { + if (page.type === 'table') { + return normalizeTablePage(page, defaults); + } + + if (page.type === 'kanban') { + return normalizeKanbanPage(page, defaults); + } + + if (page.type === 'workflow') { + return normalizeWorkflowPage(page); + } + + return { + ...page, + header: { + ...page.header, + showListIcon: page.header.showListIcon ?? false, + }, + }; +} + +function normalizeWorkflowPage( + page: HeroWorkflowPageDefinition, +): HeroWorkflowPageDefinition { + return { + ...page, + header: { + ...page.header, + actions: [], + showListIcon: false, + }, + }; +} + +export function normalizeHeroPage( + item: HeroSidebarItem, + defaults: HeroPageDefaults, +): HeroPageDefinition | null { + if (!item.page) { + return null; + } + + return normalizeExplicitPage(item.page, defaults); +} diff --git a/packages/twenty-website-new/src/sections/Hero/types/HeroHomeData.ts b/packages/twenty-website-new/src/sections/Hero/types/HeroHomeData.ts index 39932e9d3e..62569b17c0 100644 --- a/packages/twenty-website-new/src/sections/Hero/types/HeroHomeData.ts +++ b/packages/twenty-website-new/src/sections/Hero/types/HeroHomeData.ts @@ -4,6 +4,7 @@ import type { HeroBaseDataType } from '@/sections/Hero/types/HeroBaseData'; export type HeroCellText = { type: 'text'; + targetLabel?: string; value: string; shortLabel?: string; tone?: string; @@ -58,6 +59,96 @@ export type HeroRowDef = { cells: Record; }; +export type HeroDashboardMetricType = { + id: string; + title: string; + value: string; +}; + +export type HeroDashboardChartImageType = { + alt: string; + height: number; + src: string; + width: number; +}; + +export type HeroDashboardDataType = { + distributionChart: HeroDashboardChartImageType; + metrics: HeroDashboardMetricType[]; + revenueChart: HeroDashboardChartImageType; + visitsChart: HeroDashboardChartImageType; +}; + +export type HeroNavbarActionType = { + desktopOnly?: boolean; + icon: string; + label?: string; + labelTone?: 'primary' | 'secondary' | 'tertiary'; + trailingLabel?: string; + variant?: 'button' | 'icon'; +}; + +export type HeroPageHeaderType = { + actions?: string[]; + count?: number; + navbarActions?: HeroNavbarActionType[]; + showListIcon?: boolean; + title: string; +}; + +export type HeroTablePageDefinition = { + columns: HeroColumnDef[]; + header: HeroPageHeaderType; + rows: HeroRowDef[]; + type: 'table'; + width?: number; +}; + +export type HeroDashboardPageDefinition = { + dashboard: HeroDashboardDataType; + header: HeroPageHeaderType; + type: 'dashboard'; +}; + +export type HeroWorkflowPageDefinition = { + header: HeroPageHeaderType; + type: 'workflow'; +}; + +export type HeroKanbanCardType = { + accountOwner: HeroCellPerson; + amount: string; + checked?: boolean; + company: HeroCellEntity; + date: string; + id: string; + mainContact: HeroCellPerson; + rating: number; + recordId: string; + title: string; +}; + +export type HeroKanbanLaneType = { + cards: HeroKanbanCardType[]; + id: string; + label: string; + tone: string; +}; + +export type HeroKanbanPageDefinition = { + header: HeroPageHeaderType; + lanes: HeroKanbanLaneType[]; + type: 'kanban'; +}; + +export type HeroPageDefinition = + | HeroDashboardPageDefinition + | HeroKanbanPageDefinition + | HeroTablePageDefinition + | HeroWorkflowPageDefinition; + +export type HeroPageType = HeroPageDefinition['type']; + // -- Sidebar icon -- export type HeroSidebarIcon = @@ -70,6 +161,7 @@ export type HeroSidebarIcon = overlay?: 'link'; } | { + color?: string; kind: 'avatar'; label: string; tone: string; @@ -83,14 +175,11 @@ export type HeroSidebarItem = { label: string; href?: string; icon: HeroSidebarIcon; + page?: HeroPageDefinition; meta?: string; active?: boolean; showChevron?: boolean; children?: HeroSidebarItem[]; - columns?: HeroColumnDef[]; - rows?: HeroRowDef[]; - viewLabel?: string; - viewCount?: number; }; export type HeroSidebarFolder = { diff --git a/packages/twenty-website-new/src/sections/Hero/types/index.ts b/packages/twenty-website-new/src/sections/Hero/types/index.ts index c186d5606b..d79cdc8d57 100644 --- a/packages/twenty-website-new/src/sections/Hero/types/index.ts +++ b/packages/twenty-website-new/src/sections/Hero/types/index.ts @@ -1,10 +1,21 @@ export type { HeroBaseDataType } from './HeroBaseData'; export type { + HeroDashboardChartImageType, + HeroDashboardDataType, + HeroDashboardMetricType, + HeroDashboardPageDefinition, + HeroKanbanCardType, + HeroKanbanLaneType, + HeroKanbanPageDefinition, + HeroWorkflowPageDefinition, HeroCellBoolean, HeroCellEntity, HeroCellLink, HeroCellNumber, HeroCellPerson, + HeroPageDefinition, + HeroNavbarActionType, + HeroPageHeaderType, HeroCellRelation, HeroCellTag, HeroCellText, @@ -12,10 +23,12 @@ export type { HeroColumnDef, HeroHomeDataType, HeroRowDef, + HeroPageType, HeroSidebarEntry, HeroSidebarFolder, HeroSidebarIcon, HeroSidebarItem, + HeroTablePageDefinition, HeroVisualType, } from './HeroHomeData'; export type { HeroIllustrationDataType } from './HeroIllustrationData';