Files
twenty/packages/twenty-website-redone/scripts/partners-parity.mjs
T
Abdullah. 465eb05aaf Reworked website. (#21763)
twenty-website had accumulated structural problems that were cheaper to
fix by rebuilding than to refactor in place:
  
- Drift had no guardrails. Values were matched at call sites instead of
single-sourced, so things silently diverged — e.g. the radius token base
was wrong for days (every radius() consumer rendered double) because
nothing measured it against the old site's CSS variables.
- A whole tree escaped quality checks. src/lib/ (~9.8k lines) was never
format-checked, because oxfmt silently ignores directories named lib/.
- Inconsistent rhythm. Hero spacing varied 24–88px between pages
(CEO-flagged), because section spacing wasn't a token.
- Over-extraction. -config.ts sprawl pulled single-component
configuration out into the wrong place.
  
The goal: a ground-up rebuild where drift is structurally impossible,
held to a Linear / Railway / Notion / Attio quality bar. The old site is
treated as source of intent only — nothing is blindly ported; every
piece is re-decided and A/B-verified.

**Rebuild**
  
A full rebuild on Next 16 + Turbopack + Linaria (zero-runtime CSS),
~1,100 files. Marketing pages (home, product, pricing, partners +
marketplace, customers/case-studies, why-twenty, releases, legal), the
interactive AppPreview product mockup, the platform/visuals WebGL system
(engine + rigs, three code-split off every initial chunk), and the
standalone /halftone studio (the dev tool that generates the site's
halftone art — engine, exporters, and full UI ported as an isolated
island).
  
**Architecture & guarantees**
  
- Parity by construction. src/tokens/definitions.ts is the only file
with raw values; the :root CSS-variable block is generated from it at
build time and accessors derive var names through the same helpers —
derived alpha tokens appear in served CSS without ever being
hand-written.
- Mobile-first by API shape. mediaUp() is the only media helper (no
max-width helper exists, on purpose).
- Section rhythm is a token (RHYTHM.section) — the hero-spacing
inconsistency class is fixed by construction.
- Fluid type ramps interpolate font-size and line-height between
designed endpoints [390px → md]; TYPE_SCALE is the single source.
- three.js never enters an initial chunk — confined to platform/visuals
heavy zones, reached only via dynamic(ssr:false), enforced by
check-visual-bundle.
2026-06-18 11:55:24 +02:00

254 lines
8.6 KiB
JavaScript

import {
createBattery,
launchBrowser,
NEW_BASE,
openPage,
} from './battery-kit.mjs';
// Invariant battery for the partners landing. The hero/promo WebGL visuals are
// reserved frames that differ from :3002 by design, so this pins the redone's
// OWN load-bearing mechanisms rather than an A/B: the connectsUp frame seam
// (the audit flagged it as net-new behaviour on a shared, locked primitive with
// no test), the localized case-study count, and every hero's h1.
const { fail, finish, ok } = createBattery('partners-parity');
const assert = (label, condition, detail) =>
condition ? ok(label, detail) : fail(label, detail);
const browser = await launchBrowser();
// connectsUp: the promo makes the TrustedBy band above it yield its bottom
// rhythm and show overflow, so the two read as one continuous frame across the
// seam. If a future refactor of the `:has(+ [data-connect-up])` rule breaks
// this, the corner markers re-clip and the frame splits.
const partners = await openPage(browser, `${NEW_BASE}/partners`, {
viewport: { width: 1440, height: 1400 },
settleMs: 500,
});
const seam = await partners.evaluate(() => {
const band = [...document.querySelectorAll('section')].find((s) =>
/trusted by/i.test(s.textContent || ''),
);
const style = getComputedStyle(band);
return {
bottom: style.paddingBottom,
overflow: style.overflowY,
hasCount: /\d+ Case Studies/.test(document.body.textContent || ''),
};
});
assert(
'connectsUp drops the band bottom rhythm',
seam.bottom === '0px',
`padding-bottom ${seam.bottom}`,
);
assert(
'connectsUp shows the band overflow',
seam.overflow === 'visible',
`overflow-y ${seam.overflow}`,
);
assert('case-study count renders', seam.hasCount, 'no "N Case Studies"');
// Partner testimonials: a dark notched panel on a light section. The notch
// reveals the section's WHITE surface (the gray-notch defect we fixed), the
// panel fill is the dark scheme surface, and the carousel adopts the dark
// scheme so its content ink resolves to light — the half-applied scheme that
// left the name/quote/counter invisible (vars set, `color` not) is what this
// guards. The counter is centred and the quote runs sans, both matching the
// home testimonials (the quote shares the counter's sans family).
const panel = await partners.evaluate(() => {
const carousel = document.querySelector(
'[aria-label="Partner testimonials"]',
);
const section = carousel?.closest('section');
const shape = section?.querySelector('[data-card-scheme]');
const bodyFill = shape?.lastElementChild;
const counter = [...(section?.querySelectorAll('p') ?? [])].find((node) =>
/^\d+\/\d+$/.test((node.textContent ?? '').trim()),
);
const quote = section?.querySelector('h2');
return {
surface: section ? getComputedStyle(section).backgroundColor : '',
cardScheme: shape?.getAttribute('data-card-scheme') ?? '',
fill: bodyFill ? getComputedStyle(bodyFill).backgroundColor : '',
ink: carousel ? getComputedStyle(carousel).color : '',
counterAlign: counter ? getComputedStyle(counter).textAlign : '',
counterFont: counter ? getComputedStyle(counter).fontFamily : '',
quoteFont: quote ? getComputedStyle(quote).fontFamily : '',
};
});
assert(
'testimonials notch reveals a white surface',
panel.surface === 'rgb(255, 255, 255)',
panel.surface,
);
assert(
'testimonials panel fills with the dark surface',
panel.cardScheme === 'dark' && panel.fill === 'rgb(28, 28, 28)',
`${panel.cardScheme} / ${panel.fill}`,
);
assert(
'testimonials content adopts the light ink',
panel.ink === 'rgb(255, 255, 255)',
panel.ink,
);
assert(
'testimonials counter is centred',
panel.counterAlign === 'center',
panel.counterAlign,
);
assert(
'testimonials quote shares the counter sans family',
panel.quoteFont !== '' && panel.quoteFont === panel.counterFont,
`quote ${panel.quoteFont} / counter ${panel.counterFont}`,
);
// The closing sign-off: a tall centred panel whose heading carries both faces
// (serif + sans accent). "Become a partner" is the application-modal trigger (a
// button, no href); "Find a partner" links to the (Wave-B) marketplace, the
// same dangle the hero already carries. The body is measure-constrained so it
// breaks across two lines rather than running the panel width.
const signoff = await partners.evaluate(() => {
const heading = [...document.querySelectorAll('h2')].find((node) =>
/Ready to grow/.test(node.textContent ?? ''),
);
const section = heading?.closest('section');
const ctas = [...(section?.querySelectorAll('a, button') ?? [])];
const become = ctas.find((node) =>
/Become a partner/i.test(node.textContent ?? ''),
);
const find = ctas.find((node) =>
/Find a partner/i.test(node.textContent ?? ''),
);
const body = [...(section?.querySelectorAll('p') ?? [])].find((node) =>
/partner ecosystem/.test(node.textContent ?? ''),
);
return {
headingText: (heading?.textContent ?? '').replace(/\s+/g, ' ').trim(),
becomeTag: become?.tagName.toLowerCase() ?? '',
becomeHref: become?.getAttribute('href') ?? null,
findHref: find?.getAttribute('href') ?? '',
bodyWidth: body ? Math.round(body.getBoundingClientRect().width) : 0,
sectionHeight: section
? Math.round(section.getBoundingClientRect().height)
: 0,
hasCrosshair: Boolean(section?.querySelector('[data-slot="plus"]')),
};
});
assert(
'signoff heading carries both faces',
signoff.headingText.includes('Ready to grow') &&
signoff.headingText.includes('with Twenty?'),
signoff.headingText,
);
assert(
'signoff become-a-partner is a modal trigger, not a link',
signoff.becomeTag === 'button' && signoff.becomeHref === null,
`${signoff.becomeTag} href=${signoff.becomeHref}`,
);
assert(
'signoff find-a-partner links to the marketplace',
signoff.findHref.endsWith('/partners/list'),
signoff.findHref,
);
assert(
'signoff body is measure-constrained',
signoff.bodyWidth > 0 && signoff.bodyWidth <= 410,
`${signoff.bodyWidth}px`,
);
assert(
'signoff is a tall centred panel',
signoff.sectionHeight >= 759 && signoff.hasCrosshair,
`${signoff.sectionHeight}px / crosshair ${signoff.hasCrosshair}`,
);
// The application modal: clicking "Become a partner" opens a dark dialog that
// hosts the deferred wizard. The dialog carries the wizard's "Apply to build"
// title on the near-black panel, runs on the dark scheme, and its width is set
// up to ease between the form (<=720) and booking widths rather than snapping.
await partners.evaluate(() => {
const become = [...document.querySelectorAll('button')].find((node) =>
/Become a partner/i.test(node.textContent ?? ''),
);
become?.click();
});
const modal = await partners
.waitForFunction(
() =>
/Apply to build/.test(
document.querySelector('[role="dialog"]')?.textContent ?? '',
),
{ timeout: 5000 },
)
.then(() =>
partners.evaluate(() => {
const dialog = document.querySelector('[role="dialog"]');
const style = dialog ? getComputedStyle(dialog) : null;
return {
opened: Boolean(dialog),
darkScope: Boolean(dialog?.querySelector('[data-scheme="dark"]')),
panel: style?.backgroundColor ?? '',
transition: style?.transitionProperty ?? '',
width: dialog ? Math.round(dialog.getBoundingClientRect().width) : 0,
};
}),
)
.catch(() => ({
opened: false,
darkScope: false,
panel: '',
transition: '',
width: 0,
}));
assert(
'become-a-partner opens the dark application modal',
modal.opened && modal.darkScope,
`opened=${modal.opened} darkScope=${modal.darkScope}`,
);
assert(
'application modal sits on the near-black panel',
modal.panel === 'rgb(12, 12, 12)',
modal.panel,
);
assert(
'application modal eases between form and booking widths',
/width/.test(modal.transition) && modal.width <= 720,
`transition="${modal.transition}" width=${modal.width}`,
);
await partners.close();
// Every hero renders its h1 (the shared hero composition holds across schemes).
async function heroH1(path) {
const page = await openPage(browser, `${NEW_BASE}${path}`, {
viewport: { width: 1440, height: 1000 },
settleMs: 300,
});
const text = await page.evaluate(
() => document.querySelector('main h1')?.textContent || '',
);
await page.close();
return text;
}
assert(
'partners hero h1',
(await heroH1('/partners')).includes('our partner'),
'missing',
);
assert(
'why-twenty hero h1',
(await heroH1('/why-twenty')).includes('not bought'),
'missing',
);
assert(
'releases hero h1',
(await heroH1('/releases')).includes('Releases'),
'missing',
);
assert(
'customers hero h1',
(await heroH1('/customers')).includes('on Twenty'),
'missing',
);
await finish(browser);