diff --git a/packages/twenty-website-new/public/images/shared/people/avatars/sam-altman.webp b/packages/twenty-website-new/public/images/shared/people/avatars/sam-altman.webp new file mode 100644 index 0000000000..01e10dde70 Binary files /dev/null and b/packages/twenty-website-new/public/images/shared/people/avatars/sam-altman.webp differ diff --git a/packages/twenty-website-new/src/app/[locale]/product/page.tsx b/packages/twenty-website-new/src/app/[locale]/product/page.tsx index 2eb2d22a5f..fb53bd5e9d 100644 --- a/packages/twenty-website-new/src/app/[locale]/product/page.tsx +++ b/packages/twenty-website-new/src/app/[locale]/product/page.tsx @@ -98,11 +98,6 @@ export default async function ProductPage({ params }: ProductPageProps) { i18n.locale as AppLocale, )} /> - - + (null); - const rawMax = Math.max(...data.map((datum) => datum.value)); - const maxValue = rawMax * 1.3; + const allValues = data.flatMap((datum) => [datum.value, datum.value2]); + const maxValue = Math.max(...allValues) * 1.05; + const tickStep = Math.ceil(maxValue / Y_TICK_COUNT); + const yTicks = Array.from( + { length: Y_TICK_COUNT + 1 }, + (_, i) => i * tickStep, + ); return ( - Monthly volume - - {data.map((datum, index) => { - const heightPercent = (datum.value / maxValue) * 100; - const isHovered = hoveredIndex === index; + Widget name + + + Values + + {[...yTicks].reverse().map((tick) => ( + {tick === 0 ? '0' : `${tick}K`} + ))} + + + + {data.map((datum, index) => { + const h1 = (datum.value / maxValue) * 100; + const h2 = (datum.value2 / maxValue) * 100; + const isHovered = hoveredIndex === index; - return ( - - {active ? ( - - {datum.value} - - ) : null} - setHoveredIndex(index)} - onPointerLeave={() => setHoveredIndex(null)} - style={{ - backgroundColor: ACCENT, - filter: isHovered ? 'brightness(1.3)' : 'none', - height: active ? `${heightPercent}%` : '0%', - transitionDelay: active ? `${index * 50}ms` : '0ms', - }} - /> - {datum.label} - - ); - })} - + return ( + setHoveredIndex(index)} + onPointerLeave={() => setHoveredIndex(null)} + > + + + {active ? ( + + {datum.value} + + ) : null} + + + + {active ? ( + + {datum.value2} + + ) : null} + + + + {datum.label} + + ); + })} + + Months + + + ); } diff --git a/packages/twenty-website-new/src/sections/Feature/visuals/DashboardVisual.tsx b/packages/twenty-website-new/src/sections/Feature/visuals/DashboardVisual.tsx index bd9c40cae3..f04866141b 100644 --- a/packages/twenty-website-new/src/sections/Feature/visuals/DashboardVisual.tsx +++ b/packages/twenty-website-new/src/sections/Feature/visuals/DashboardVisual.tsx @@ -5,20 +5,109 @@ import { styled } from '@linaria/react'; import { BarChart } from './BarChart'; import { BAR_DATA, DONUT_VALUE } from './dashboard-visual.data'; import { DonutChart } from './DonutChart'; -import { WindowChrome } from './WindowChrome'; +import { + BG_DARK, + BG_PANEL, + BORDER_COLOR, + TEXT_MUTED, + TEXT_PRIMARY, + TEXT_SECONDARY, +} from './visual-tokens'; + +const Window = styled.div` + background-color: ${BG_DARK}; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + width: 100%; +`; + +const Topbar = styled.div` + align-items: center; + border-bottom: 1px solid ${BORDER_COLOR}; + display: flex; + flex-shrink: 0; + gap: 8px; + padding: 10px 16px; +`; + +const BreadcrumbNav = styled.div` + align-items: center; + display: flex; + gap: 6px; +`; + +const BreadcrumbIcon = styled.span` + align-items: center; + color: ${TEXT_SECONDARY}; + display: flex; +`; + +const BreadcrumbText = styled.span` + color: ${TEXT_SECONDARY}; + font-size: 12px; + letter-spacing: 0.01em; +`; + +const BreadcrumbBold = styled.span` + color: ${TEXT_PRIMARY}; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.01em; +`; + +const TopbarActions = styled.div` + align-items: center; + display: flex; + gap: 8px; + margin-left: auto; +`; + +const ActionBtn = styled.span` + align-items: center; + border: 1px solid ${BORDER_COLOR}; + border-radius: 4px; + color: ${TEXT_SECONDARY}; + display: flex; + font-size: 11px; + gap: 4px; + padding: 4px 8px; +`; + +const ActionIcon = styled.span` + color: ${TEXT_MUTED}; + display: flex; +`; const Body = styled.div` display: grid; flex: 1; gap: 12px; grid-template-columns: 200px 1fr; + grid-template-rows: 1fr 60px; min-height: 0; padding: 14px; +`; - @media (max-width: 600px) { - grid-template-columns: 1fr; - grid-template-rows: auto 1fr; - } +const BarChartCell = styled.div` + grid-column: 2; + grid-row: 1 / -1; + min-height: 0; +`; + +const BottomPanel = styled.div` + background-color: ${BG_PANEL}; + border: 1px solid ${BORDER_COLOR}; + border-radius: 10px; + padding: 14px 16px; +`; + +const WidgetTitle = styled.span` + color: ${TEXT_SECONDARY}; + font-size: 12px; + font-weight: 500; + letter-spacing: 0.02em; `; type DashboardVisualProps = { @@ -27,15 +116,61 @@ type DashboardVisualProps = { export function DashboardVisual({ active }: DashboardVisualProps) { return ( - + + + + + + + + + + + + Dashboard / + Sales performances + + + + + + + + Delete + + + + + + + + + ⌘K + + - + + + + + Widget name + - + ); } diff --git a/packages/twenty-website-new/src/sections/Feature/visuals/DonutChart.tsx b/packages/twenty-website-new/src/sections/Feature/visuals/DonutChart.tsx index 8fda9580ae..7b42a0ef21 100644 --- a/packages/twenty-website-new/src/sections/Feature/visuals/DonutChart.tsx +++ b/packages/twenty-website-new/src/sections/Feature/visuals/DonutChart.tsx @@ -92,7 +92,7 @@ export function DonutChart({ active, value }: DonutChartProps) { return ( - Conversion rate + Widget name }> - - - ); -} diff --git a/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/ProductVisual.tsx b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/ProductVisual.tsx new file mode 100644 index 0000000000..bfee5f9421 --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/ProductVisual.tsx @@ -0,0 +1,469 @@ +'use client'; + +import { styled } from '@linaria/react'; + +import type { AppPreviewConfig } from '@/sections/AppPreview'; +import { COLORS } from '@/sections/AppPreview/Shared/utils/app-preview-theme'; +import { VISUAL_TOKENS } from '@/sections/AppPreview/Shared/utils/app-preview-tokens'; +import { AppPreviewNavbar } from '@/sections/AppPreview/Shell/AppPreviewNavbar'; +import { AppPreviewSidebar } from '@/sections/AppPreview/Shell/AppPreviewSidebar'; +import { AppPreviewViewbar } from '@/sections/AppPreview/Shell/AppPreviewViewbar'; +import { renderPageDefinition } from '@/sections/AppPreview/Shell/PageRenderers'; +import { AppWindow } from '@/sections/AppPreview/AppWindow/AppWindow'; +import { WindowOrderProvider } from '@/sections/AppPreview/WindowOrder/WindowOrderProvider'; +import { theme } from '@/theme'; + +import { PROMPT_OPTIONS } from './product-visual.data'; +import { useProductVisualAutoplay } from './use-product-visual-autoplay'; + +const StyledRoot = styled.div` + isolation: isolate; + margin-top: ${theme.spacing(5)}; + position: relative; + text-align: left; + width: 100%; + + @media (min-width: ${theme.breakpoints.md}px) { + margin-top: ${theme.spacing(8)}; + } +`; + +const ShellScene = styled.div` + aspect-ratio: 1 / 1; + margin: 0 auto; + max-height: 740px; + position: relative; + width: 100%; + + @media (min-width: ${theme.breakpoints.md}px) { + aspect-ratio: 1280 / 832; + } +`; + +const AppLayout = styled.div` + display: flex; + flex: 1 1 auto; + height: 100%; + min-height: 0; + min-width: 0; + overflow: hidden; + position: relative; + width: 100%; + z-index: 1; +`; + +const RightColumn = styled.div` + display: flex; + flex: 1 1 0; + flex-direction: column; + gap: 12px; + min-height: 0; + min-width: 0; + padding: 12px 12px 12px 0; +`; + +const ContentRow = styled.div` + display: flex; + flex: 1 1 auto; + gap: 8px; + min-height: 0; +`; + +const IndexSurface = styled.div` + background: ${COLORS.background}; + border: 1px solid ${COLORS.border}; + border-radius: 8px; + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; + min-width: 0; + overflow: hidden; + + [aria-label*='workflow'] > div > div { + left: 0; + transform: scale(0.65) translateX(-20%); + transform-origin: top left; + } +`; + +const AiPanel = styled.aside` + background: ${VISUAL_TOKENS.background.primary}; + border: 1px solid ${VISUAL_TOKENS.border.color.medium}; + border-radius: 8px; + display: flex; + flex-direction: column; + flex-shrink: 0; + height: 100%; + min-height: 0; + overflow: hidden; + width: 280px; + + @media (max-width: ${theme.breakpoints.md}px) { + display: none; + } +`; + +const AiPanelHeader = styled.div` + align-items: center; + background-color: ${VISUAL_TOKENS.background.secondary}; + border-bottom: 1px solid ${VISUAL_TOKENS.border.color.medium}; + display: flex; + flex-shrink: 0; + gap: 4px; + height: 40px; + padding: 0 8px; +`; + +const AiHeaderBtn = styled.span` + align-items: center; + border-radius: 4px; + color: ${VISUAL_TOKENS.font.color.secondary}; + display: flex; + height: 28px; + justify-content: center; + width: 28px; +`; + +const AiPanelTitle = styled.span` + color: ${VISUAL_TOKENS.font.color.primary}; + flex: 1; + font-size: 13px; + font-weight: 600; + text-align: center; +`; + +const AiMessages = styled.div` + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; + overflow-y: auto; + padding: 12px; +`; + +const UserMsg = styled.div` + background: ${VISUAL_TOKENS.background.transparent.medium}; + border-radius: ${VISUAL_TOKENS.border.radius.sm}; + color: ${VISUAL_TOKENS.font.color.secondary}; + font-size: 13px; + font-weight: 500; + line-height: 1.4em; + padding: 4px 8px; + width: fit-content; +`; + +const AiMsg = styled.div` + color: ${VISUAL_TOKENS.font.color.primary}; + font-size: 13px; + font-weight: 400; + line-height: 1.4em; + width: 100%; +`; + +const ThinkingText = styled.span` + color: ${VISUAL_TOKENS.font.color.tertiary}; + font-size: 13px; +`; + +const PromptOption = styled.button` + align-items: center; + background: none; + border: none; + border-radius: 4px; + color: ${VISUAL_TOKENS.font.color.primary}; + cursor: pointer; + display: flex; + font-size: 13px; + gap: 8px; + line-height: 1.4; + padding: 6px 4px; + text-align: left; + width: 100%; + + &:hover { + background: ${VISUAL_TOKENS.background.transparent.light}; + } +`; + +const PromptOptionIcon = styled.span` + align-items: center; + color: ${VISUAL_TOKENS.font.color.secondary}; + display: flex; + flex-shrink: 0; +`; + +const PromptOptions = styled.div` + display: flex; + flex-direction: column; + padding: 0 12px; +`; + +const AiInputArea = styled.div` + align-items: flex-end; + display: flex; + flex-direction: column; + flex-shrink: 0; + gap: 8px; + padding: 12px; +`; + +const AiInputBox = styled.div` + background-color: ${VISUAL_TOKENS.background.transparent.lighter}; + border: 1px solid ${VISUAL_TOKENS.border.color.medium}; + border-radius: 8px; + display: flex; + flex-direction: column; + min-height: 100px; + padding: 12px; + width: 100%; +`; + +const AiInputPlaceholder = styled.span` + color: ${VISUAL_TOKENS.font.color.light}; + font-size: 13px; + font-weight: 400; +`; + +const AiInputBtnRow = styled.div` + align-items: center; + display: flex; + justify-content: space-between; + margin-top: auto; +`; + +const AiInputLeftBtns = styled.div` + align-items: center; + color: ${VISUAL_TOKENS.font.color.tertiary}; + display: flex; + gap: 2px; +`; + +const AiInputRightBtns = styled.div` + align-items: center; + display: flex; + gap: 4px; +`; + +const ModelChip = styled.span` + align-items: center; + border: 1px solid ${VISUAL_TOKENS.border.color.medium}; + border-radius: 6px; + color: ${VISUAL_TOKENS.font.color.secondary}; + display: flex; + font-size: 11px; + gap: 4px; + padding: 3px 8px; +`; + +const SendBtn = styled.span` + align-items: center; + background: ${VISUAL_TOKENS.border.color.medium}; + border-radius: 50%; + color: ${VISUAL_TOKENS.font.color.tertiary}; + display: flex; + height: 24px; + justify-content: center; + width: 24px; +`; + +type ProductVisualProps = { + visual: AppPreviewConfig; +}; + +export function ProductVisual({ visual }: ProductVisualProps) { + const { + activeItem, + activeLabel, + displayPage, + handleOptionSelect, + handleSelectLabel, + handleToggleFolder, + highlightedItemId, + openFolderIds, + revealedObjectIds, + selectedOption, + streamComplete, + streamedText, + workspaceNav, + } = useProductVisualAutoplay(visual); + + const activeHeader = displayPage?.header; + const showViewBar = + displayPage !== null && + displayPage !== undefined && + displayPage.type !== 'dashboard' && + displayPage.type !== 'workflow'; + + return ( + + + + + + + + + + + + + {showViewBar ? ( + + ) : null} + + {displayPage + ? renderPageDefinition( + displayPage, + handleSelectLabel, + activeItem?.id ?? activeLabel, + ) + : null} + + + + + + + + + + + Ask AI + + + + + + + + + {PROMPT_OPTIONS[selectedOption].label} + {streamedText ? ( + {streamedText} + ) : ( + Thinking... + )} + + {streamComplete ? ( + + {PROMPT_OPTIONS.filter( + (_, index) => index !== selectedOption, + ).map((option, index) => ( + + handleOptionSelect(PROMPT_OPTIONS.indexOf(option)) + } + > + {option.icon} + {option.label} + + ))} + + ) : null} + + + + Ask, search or make anything... + + + + + + + + + + + + + Claude Haiku 4.5 + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/product-visual.data.tsx b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/product-visual.data.tsx new file mode 100644 index 0000000000..583b8617fe --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/product-visual.data.tsx @@ -0,0 +1,127 @@ +import { SHARED_PEOPLE_AVATAR_URLS } from '@/content/site/asset-paths'; + +export const NEW_COMPANY_ROW = { + id: 'openai', + cells: { + company: { type: 'entity' as const, name: 'OpenAI', domain: 'openai.com' }, + url: { type: 'link' as const, value: 'openai.com' }, + createdBy: { + type: 'person' as const, + name: 'AI Agent', + tone: 'gray' as const, + kind: 'system' as const, + shortLabel: 'AI', + }, + address: { type: 'text' as const, value: '3180 18th St' }, + accountOwner: { + type: 'person' as const, + name: 'Sam Altman', + tone: 'amber' as const, + kind: 'person' as const, + avatarUrl: SHARED_PEOPLE_AVATAR_URLS.samAltman, + }, + icp: { type: 'boolean' as const, value: true }, + arr: { type: 'number' as const, value: '$2,000,000' }, + linkedin: { type: 'link' as const, value: 'openai' }, + industry: { type: 'tag' as const, value: 'AI Research' }, + mainContact: { + type: 'person' as const, + name: 'Sam Altman', + shortLabel: 'S', + tone: 'amber' as const, + kind: 'person' as const, + avatarUrl: SHARED_PEOPLE_AVATAR_URLS.samAltman, + }, + employees: { type: 'number' as const, value: '3,500' }, + opportunities: { type: 'relation' as const, items: [] }, + added: { type: 'text' as const, value: 'Just now' }, + }, +}; + +export const NEW_PERSON_ROW = { + id: 'sam-altman', + cells: { + name: { + type: 'person' as const, + name: 'Sam Altman', + tone: 'amber' as const, + kind: 'person' as const, + avatarUrl: SHARED_PEOPLE_AVATAR_URLS.samAltman, + }, + company: { type: 'entity' as const, name: 'OpenAI', domain: 'openai.com' }, + email: { type: 'link' as const, value: 'sam@openai.com' }, + phone: { type: 'text' as const, value: '+1 415 555 0199' }, + jobTitle: { type: 'text' as const, value: 'CEO' }, + city: { type: 'text' as const, value: 'San Francisco' }, + linkedin: { type: 'link' as const, value: 'sama' }, + added: { type: 'text' as const, value: 'Just now' }, + }, +}; + +export const PROMPT_OPTIONS = [ + { + icon: ( + + + + + ), + label: 'Add a new lead', + navSteps: [ + { at: 0.25, target: 'Companies' }, + { at: 0.65, target: 'People' }, + ], + response: + 'Adding OpenAI as a new company. Setting domain to openai.com, industry to AI Research, and ARR to $2,000,000. Account owner assigned to Sam Altman. Company record is live in your CRM.\n\nNow creating the contact — adding Sam Altman as CEO at OpenAI, based in San Francisco. Person record linked to the company.', + }, + { + icon: ( + + + + + + + ), + label: 'Create a dashboard', + navSteps: [ + { at: 0.35, target: 'Dashboards' }, + { at: 0.75, target: 'Sales Dashboard' }, + ], + response: + "Building a Sales Performance dashboard. I'll include revenue by month, new subscriptions, churn rate, and a distribution chart. Navigating to dashboards now... Opening the Sales Dashboard — your metrics are ready.", + }, + { + icon: ( + + + + + ), + label: 'Set up a workflow', + navSteps: [{ at: 0.45, target: 'Create company when adding a new person' }], + response: + 'Creating an automation: when a new person is added, automatically create their company record and link them. Setting the trigger and actions now... Workflow is active and ready to run.', + }, +]; diff --git a/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/use-product-visual-autoplay.ts b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/use-product-visual-autoplay.ts new file mode 100644 index 0000000000..3b2001e0e5 --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/use-product-visual-autoplay.ts @@ -0,0 +1,118 @@ +import { useCallback, useEffect, useState } from 'react'; + +import type { AppPreviewConfig } from '@/sections/AppPreview'; +import { useAppPreviewState } from '@/sections/AppPreview/Shell/use-app-preview-state'; + +import { + NEW_COMPANY_ROW, + NEW_PERSON_ROW, + PROMPT_OPTIONS, +} from './product-visual.data'; + +export function useProductVisualAutoplay(visual: AppPreviewConfig) { + const [selectedOption, setSelectedOption] = useState(0); + const [streamedText, setStreamedText] = useState(''); + const [streamComplete, setStreamComplete] = useState(false); + const [companyAdded, setCompanyAdded] = useState(false); + const [personAdded, setPersonAdded] = useState(false); + + const { + activeItem, + activeLabel, + activePage, + handleSelectLabel, + handleToggleFolder, + highlightedItemId, + openFolderIds, + revealedObjectIds, + workspaceNav, + } = useAppPreviewState(visual); + + let displayPage = activePage; + if ( + activePage !== null && + activePage !== undefined && + activePage.type === 'table' + ) { + const title = activePage.header?.title; + if (companyAdded && title === 'All Companies') { + displayPage = { + ...activePage, + header: { + ...activePage.header, + count: (activePage.header.count ?? 0) + 1, + }, + rows: [NEW_COMPANY_ROW, ...activePage.rows], + }; + } else if (personAdded && title === 'All People') { + displayPage = { + ...activePage, + header: { + ...activePage.header, + count: (activePage.header.count ?? 0) + 1, + }, + rows: [NEW_PERSON_ROW, ...activePage.rows], + }; + } + } + + useEffect(() => { + const option = PROMPT_OPTIONS[selectedOption]; + const fullText = option.response; + let index = 0; + const completedSteps = new Set(); + let companyInjected = false; + let personInjected = false; + setStreamedText(''); + setStreamComplete(false); + setCompanyAdded(false); + setPersonAdded(false); + const interval = setInterval(() => { + index += 1; + setStreamedText(fullText.slice(0, index)); + const progress = index / fullText.length; + option.navSteps.forEach((step, stepIndex) => { + if (!completedSteps.has(stepIndex) && progress >= step.at) { + completedSteps.add(stepIndex); + handleSelectLabel(step.target); + } + }); + if (selectedOption === 0) { + if (!companyInjected && progress >= 0.2) { + companyInjected = true; + setCompanyAdded(true); + } + if (!personInjected && progress >= 0.6) { + personInjected = true; + setPersonAdded(true); + } + } + if (index >= fullText.length) { + clearInterval(interval); + setStreamComplete(true); + } + }, 20); + return () => clearInterval(interval); + }, [selectedOption, handleSelectLabel]); + + const handleOptionSelect = useCallback( + (optionIndex: number) => setSelectedOption(optionIndex), + [], + ); + + return { + activeItem, + activeLabel, + displayPage, + handleOptionSelect, + handleSelectLabel, + handleToggleFolder, + highlightedItemId, + openFolderIds, + revealedObjectIds, + selectedOption, + streamComplete, + streamedText, + workspaceNav, + }; +} diff --git a/packages/twenty-website-new/src/sections/Hero/components/index.ts b/packages/twenty-website-new/src/sections/Hero/components/index.ts index a65c978fb5..e90d200c8b 100644 --- a/packages/twenty-website-new/src/sections/Hero/components/index.ts +++ b/packages/twenty-website-new/src/sections/Hero/components/index.ts @@ -3,7 +3,7 @@ import { Cta } from './Cta'; import { Heading } from './Heading'; import { AppPreview } from '@/sections/AppPreview'; import { PartnerVisual } from './PartnerVisual/PartnerVisual'; -import { ProductVisual } from './ProductVisual'; +import { ProductVisual } from './ProductVisual/ProductVisual'; import { ReleaseNotesVisual } from './ReleaseNotesVisual'; import { Root } from './Root'; import { WhyTwentyVisual } from './WhyTwentyVisual';