feat(website): swap product Demo for a register Signoff; make signoff headings width-driven (#22232)

## Summary

Reworks the product page's closing section, and in doing so makes every
signoff closer width-driven.

### 1. Replace the Demo section with a register Signoff (`6a5f0c34bf`)

The product page ended on a static `AppPreview` mockup ("Try it live").
Swap it for the shared `Signoff` closer — a focused register CTA, the
same component the customers / why-twenty / partners pages use — so the
page ends by driving sign-ups instead of re-showing the product.

- New `ProductSignoff`: heading "Start moving faster today.", a
supporting line, and the **Get started + Talk to us** pair (matching
`CustomersCatalogSignoff`). Uses `SITE_URLS.appWelcome` per the
site-urls rule rather than inlining the URL.
- Removes the now-unused `product-demo` section and its 192KB background
asset.

### 2. Make Signoff headings width-driven (`0365c0d2ea`)

The signoff headings forced their two-line break with a literal `\n` in
the translated string — against the site's typography principle (fluid
type + `text-wrap: balance`, no `<br>`).

- `Signoff` now carries one default **`615px`** heading measure; all
five closers drop `\n`.
- The width was **measured, not guessed**: each heading was rendered
headlessly in the real production fonts (Aleo 300 body, Host Grotesk 300
accent — each heading mixes both) at the desktop size with `text-wrap:
balance`, then I found the common window where every heading breaks
identically: whyTwenty `[565–820]`, customers `[425–800]`, partner
`[380–730]`, product `[380–660]` — `615` sits in all of them. Each was
then verified to reproduce its exact current break at 615px.
- **Affects 4 other pages** (why-twenty, customers, customers/[slug],
partners) — all verified to render identically, and headings now reflow
on narrow viewports instead of being pinned by `\n`.
- Drops the one-off `headingMaxWidth` prop (now unused).

## Test plan

- oxfmt, oxlint, check-conventions, typecheck — all green.
- Headless measurement confirms all five signoff headings reproduce
their current desktop break at 615px (no visual regression).
- Visually confirmed the product closer and the four sibling signoffs.
This commit is contained in:
Abdullah.
2026-06-26 21:45:39 +05:00
committed by GitHub
parent e747bc3e42
commit d58ec64e10
11 changed files with 38 additions and 118 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

@@ -12,9 +12,9 @@ import {
} from '@/platform/seo';
import { Faq } from '@/sections/faq';
import { Menu } from '@/sections/menu';
import { ProductDemo } from '@/sections/product-demo';
import { ProductFeature } from '@/sections/product-feature';
import { ProductHero } from '@/sections/product-hero';
import { ProductSignoff } from '@/sections/product-signoff';
import { ProductStepper } from '@/sections/product-stepper';
import { ProductThreeCards } from '@/sections/three-cards';
import { TrustedBy } from '@/sections/trusted-by';
@@ -51,7 +51,7 @@ export default async function ProductPage({
<ProductFeature />
<ProductThreeCards />
<ProductStepper />
<ProductDemo />
<ProductSignoff />
<Faq />
</main>
</MenuStyleProvider>
@@ -12,7 +12,7 @@ export function CustomersCaseStudySignoff() {
body={i18n._(
msg`Join the teams that chose to own their CRM. Start building with Twenty today.`,
)}
heading={i18n._(msg`Ready to grow\n*with Twenty?*`)}
heading={i18n._(msg`Ready to grow *with Twenty?*`)}
scheme="muted"
>
<Button
@@ -12,7 +12,7 @@ export function CustomersCatalogSignoff() {
body={i18n._(
msg`Join the teams that chose to own their CRM. Start building with Twenty today.`,
)}
heading={i18n._(msg`Ready to build\n*your own story?*`)}
heading={i18n._(msg`Ready to build *your own story?*`)}
scheme="light"
>
<Button
@@ -12,7 +12,7 @@ export function PartnerSignoff() {
body={i18n._(
msg`Join our partner ecosystem and help businesses take control of their CRM.`,
)}
heading={i18n._(msg`Ready to grow\n*with Twenty?*`)}
heading={i18n._(msg`Ready to grow *with Twenty?*`)}
scheme="light"
>
<BecomePartnerButton label={msg`Become a partner`} variant="outlined" />
@@ -1,108 +0,0 @@
import { msg } from '@lingui/core/macro';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import NextImage from 'next/image';
import { AppPreview } from '@/app-preview/AppPreview';
import { getServerI18n } from '@/platform/i18n/get-server-i18n';
import { APP_PREVIEW_STAGE } from '@/tokens/app-preview/app-preview-stage';
import { HERO_COMPOSITION, spacing } from '@/tokens';
import { Button, Eyebrow, Heading, SectionIntro, SectionShell } from '@/ui';
// The closer: the full product mockup in its static frame under a "try
// it live" intro, with the dash pattern fading in behind the lower half
// of the section (the shell's background slot, as on the home hero).
const PatternLayer = styled.div`
bottom: 0;
height: 60%;
left: 50%;
opacity: 0.6;
pointer-events: none;
position: absolute;
transform: translateX(-50%);
width: 100%;
`;
const patternImageClassName = css`
object-fit: cover;
object-position: center top;
`;
/* Block flow, not grid: a grid would blockify the inline-flex eyebrow
and stretch it off-center. text-align centers every child as one
mechanism. */
const IntroMeasure = styled.div`
margin-inline: auto;
text-align: center;
& > * + * {
margin-top: ${spacing(6)};
}
`;
// The old site hard-breaks the heading after "a"; the measure makes the
// same two-line break emerge (hero precedent — no <br>).
const HeadingMeasure = styled.div`
max-width: 560px;
`;
// The mockup hangs at the hero's CTA-to-window measure, not the stack
// rhythm (user-ratified: the two CTA-over-mockup moments read the same).
const PreviewRoot = styled.div`
margin-top: ${HERO_COMPOSITION.ctaToVisualGapPx}px;
width: 100%;
`;
// Fixed at the scene's full width, not a responsive fit: on viewports narrower
// than the scene the mockup stays full-size and bleeds past the gutter, clipping
// at the section edge (the shell's overflow: clip) — the old site's full-size
// demo rather than a scaled-down one. The stage token is the single source.
const SceneBox = styled.div`
aspect-ratio: ${APP_PREVIEW_STAGE.windowScene.widthPx} /
${APP_PREVIEW_STAGE.windowScene.heightPx};
margin-inline: auto;
width: ${APP_PREVIEW_STAGE.windowScene.widthPx}px;
`;
export function ProductDemo() {
const i18n = getServerI18n();
return (
<SectionShell
background={
<PatternLayer aria-hidden>
<NextImage
alt=""
className={patternImageClassName}
fill
sizes="(min-width: 921px) 100vw"
src="/images/product/demo/background.webp"
/>
</PatternLayer>
}
scheme="light"
>
<div>
<SectionIntro>
<IntroMeasure>
<Eyebrow>{i18n._(msg`Try it live`)}</Eyebrow>
<HeadingMeasure>
<Heading as="h2" size="lg" weight="light">
{i18n._(msg`A demo worth a *thousand words*`)}
</Heading>
</HeadingMeasure>
<Button
href="https://app.twenty.com/welcome"
label={i18n._(msg`Try Twenty Cloud`)}
/>
</IntroMeasure>
</SectionIntro>
<PreviewRoot>
<SceneBox>
<AppPreview mode="static" />
</SceneBox>
</PreviewRoot>
</div>
</SectionShell>
);
}
@@ -1 +0,0 @@
export { ProductDemo } from './ProductDemo';
@@ -0,0 +1,21 @@
import { msg } from '@lingui/core/macro';
import { TalkToUsButton } from '@/contact-cal';
import { getServerI18n } from '@/platform/i18n/get-server-i18n';
import { SITE_URLS } from '@/platform/site-urls';
import { Button, Signoff } from '@/ui';
export function ProductSignoff() {
const i18n = getServerI18n();
return (
<Signoff
body={i18n._(msg`Open-source, AI-ready, and set up in minutes.`)}
heading={i18n._(msg`Start moving *faster today.*`)}
scheme="light"
>
<Button href={SITE_URLS.appWelcome} label={i18n._(msg`Get started`)} />
<TalkToUsButton label={msg`Talk to us`} variant="outlined" />
</Signoff>
);
}
@@ -0,0 +1 @@
export { ProductSignoff } from './ProductSignoff';
@@ -10,7 +10,7 @@ export function WhyTwentySignoff() {
<Signoff
body={i18n._(msg`Open-source, AI-ready, and yours to shape.`)}
crosshairSide="left"
heading={i18n._(msg`Build a CRM your\ncompetitors *can't buy.*`)}
heading={i18n._(msg`Build a CRM your competitors *can't buy.*`)}
scheme="dark"
>
<Button
+10 -3
View File
@@ -30,6 +30,11 @@ const SignoffStack = styled.div`
}
`;
const HeadingMeasure = styled.div`
max-width: 615px;
width: 100%;
`;
// Measure tuned to a clean break without coupling a <br> into the translation.
const Subline = styled.div`
margin-top: ${spacing(2)};
@@ -72,9 +77,11 @@ export function Signoff({
scheme={scheme}
>
<SignoffStack>
<Heading as="h2" size="lg" weight="light">
{heading}
</Heading>
<HeadingMeasure>
<Heading as="h2" size="lg" weight="light">
{heading}
</Heading>
</HeadingMeasure>
<Subline>
<Body muted size="sm">
{body}