Skip to content

Commit 7e034f7

Browse files
authored
feat(website): surface partner Categories (partnerScope) in marketplace, drop deploymentExpertise facet (twentyhq#21127)
## What Rebinds the marketplace's expertise facet from `deploymentExpertise` (Cloud / Self-host) to **`partnerScope`** — the five partner Categories: Advisory & Discovery · Solutioning · Custom Development · Hosting & Infrastructure · Training & Adoption. Moves the card chip, the profile facts row, the dropdown filter, the `?categories=` URL param, and the API-boundary normalization onto `partnerScope`. The standalone Cloud/Self-host facet is **dropped** (hosting is now the `HOSTING` category), per the harmonization decision. ## Depends on - The app exposing `partnerScope` — companion app PR twentyhq#21126. - The new `partnerScope` options + data migration — signup app PR twentyhq#21040. ## Tests TDD red→green on: `filter-partners`, both API normalizers, `filter-url-helpers`, `PartnerCard`, `use-filter-state`. 53/53 pass; typecheck + lint + format clean. ## Merge order (we'll decide) Independent diff. Suggested last of the four, after the signup PRs (twentyhq#21039 / twentyhq#21040) and the app PR (twentyhq#21126). Run `lingui:extract` once after twentyhq#21039 merges so the `.po` files don't conflict twice. Deploy the app + migrate before the website ships.
1 parent 94d2e38 commit 7e034f7

41 files changed

Lines changed: 2429 additions & 216 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DESIGN.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Twenty Website — DESIGN.md
2+
3+
> Visual system for the Twenty marketing site. Distilled from `packages/twenty-website/src/theme/`. Loaded by every `impeccable` invocation alongside PRODUCT.md.
4+
5+
## Theme
6+
7+
**Light by default.** A founder browsing a partner profile in daylight on a 14–27 inch monitor is the default scene. The site does ship a `data-scheme="dark"` override (see `css-variables.ts`), but no current public page opts into it. Treat dark as a deferred surface.
8+
9+
## Color
10+
11+
Palette is OKLCH-equivalent neutrals at the surface level. The brand accents (blue, pink, yellow, green) are present in the token system but used sparingly — none of them appear on the partner pages.
12+
13+
### Strategy: Restrained
14+
15+
Tinted neutrals + one accent ≤10%. The accent for partner pages is the deep ink black (`var(--color-black-100)`) used in CTAs and hover states. Anything beyond a hairline border, an icon glyph, or a primary CTA should question whether it needs color at all.
16+
17+
### Tokens (from `src/theme/colors.ts` + `css-variables.ts`)
18+
19+
Neutrals (the workhorses):
20+
21+
| Token | Hex (computed) | Role |
22+
| --- | --- | --- |
23+
| `colors.primary.background[100]` | `#ffffff` | Page + card surface |
24+
| `colors.primary.text[100]` | `#1c1c1c` | Headlines, primary text |
25+
| `colors.primary.text[80]` | `#1c1c1ccc` | Body text |
26+
| `colors.primary.text[60]` | `#1c1c1c99` | Eyebrows, meta, captions |
27+
| `colors.primary.text[40]` | `#1c1c1c66` | Disabled / placeholder |
28+
| `colors.primary.text[20]` | `#1c1c1c33` | Subtle separators |
29+
| `colors.primary.text[10]` | `#1c1c1c1a` | Hairline borders |
30+
| `colors.primary.text[5]` | `#1c1c1c0d` | Subtle fills (rates panel, skill chips) |
31+
| `colors.primary.border[10]` | `#1c1c1c1a` | Default border |
32+
| `colors.primary.border[20]` | `#1c1c1c33` | Hover border |
33+
34+
Reverse palette (for dark CTAs):
35+
36+
| Token | Role |
37+
| --- | --- |
38+
| `colors.secondary.background[100]` | Filled CTA background (deep ink) |
39+
| `colors.secondary.text[100]` | Filled CTA text (white) |
40+
41+
Brand accents (currently absent from partner pages; available if needed):
42+
43+
- `colors.accent.blue``#4a38f5` / `#8174f8`
44+
- `colors.accent.pink``#ed87fc` / `#f3abfd`
45+
- `colors.accent.yellow``#feffb7` / `#feffd9`
46+
- `colors.accent.green``#89fc9a` / `#b0fdbe`
47+
- `colors.highlight` — same hue as blue accent
48+
49+
**Do not introduce gradients, glass blurs, or saturated fills on partner pages.** Color is conviction here, not decoration.
50+
51+
## Typography
52+
53+
Three families, each load-balanced via CSS variables:
54+
55+
| Family | Var | Use |
56+
| --- | --- | --- |
57+
| `theme.font.family.serif` | `--font-serif` | Headlines, partner names, headline values |
58+
| `theme.font.family.sans` | `--font-sans` | Body, prose, interactive labels |
59+
| `theme.font.family.mono` | `--font-mono` | Eyebrows, meta, currency labels, tabular numerics |
60+
| `theme.font.family.retro` | `--font-retro` | Reserved (not used on partner pages) |
61+
62+
### Weight + Size Contrast
63+
64+
Weights: `light: 300`, `regular: 400`, `medium: 500`. No bold. Hierarchy is driven by scale and family contrast, never by weight alone.
65+
66+
Scale (`theme.font.size(n)``calc(var(--font-base) * n)`, where `--font-base: 0.25rem` ≈ 4px):
67+
68+
- Display / h1: size 9–12 (36–48px)
69+
- h2 / section heads: size 7–8 (28–32px)
70+
- h3 / card heads: size 5–6 (20–24px)
71+
- Body / prose: size 4–5 (16–20px)
72+
- Eyebrow / meta: size 3 (12px) with `letter-spacing: 0.06–0.08em` and `text-transform: uppercase`
73+
74+
Body line length: cap at 65–75ch (the existing `PartnerProfileIntro` uses `max-width: 62ch` — keep that order of magnitude).
75+
76+
### Hierarchy contract
77+
78+
- A serif `<h1>` at size 9 light reads as a partner's name on the detail page.
79+
- A mono eyebrow above or below it locates the partner (region · city · country).
80+
- A serif size 6 light reads as a section head.
81+
- Body prose is sans regular.
82+
- Currency values are serif (they read as headline numbers, not stats).
83+
- Currency labels and meta are mono.
84+
85+
## Spacing & Layout
86+
87+
Base unit `4px`. Spacing helper `theme.spacing(n)` returns `n * 4px`. Common rhythms on the partner pages:
88+
89+
- Inter-section gap on the detail page: `theme.spacing(10–14)` — generous, editorial breathing room.
90+
- Inter-element gap inside a section: `theme.spacing(3–5)`.
91+
- Card padding: `theme.spacing(6)`.
92+
- Page horizontal padding: `theme.spacing(4)` mobile, `theme.spacing(10)` ≥ md breakpoint.
93+
94+
### Radius
95+
96+
`theme.radius(n)` returns `n * 2px`. The default card radius is `theme.radius(2)` = 4px. Pills use `999px`. No softer rounding than that.
97+
98+
### Borders
99+
100+
Borders are hairline (`1px solid theme.colors.primary.border[10]`). They define edges quietly. On hover they step to `border[20]`. Never use a chunky border as decoration.
101+
102+
## Components
103+
104+
### Card (PartnerCard, RatesPanel)
105+
106+
White surface, hairline border, 4px radius, 24px padding, soft shadow on hover only:
107+
108+
```css
109+
background-color: ${theme.colors.primary.background[100]};
110+
border: 1px solid ${theme.colors.primary.border[10]};
111+
border-radius: ${theme.radius(2)};
112+
padding: ${theme.spacing(6)};
113+
114+
&:hover {
115+
border-color: ${theme.colors.primary.border[20]};
116+
box-shadow: 0 12px 32px -16px rgba(0, 0, 0, 0.18);
117+
transform: translateY(-2px);
118+
}
119+
```
120+
121+
### Chip / Pill
122+
123+
Rounded `999px`, 1px border, subtle background fill (`primary.text[5]` for filter pills, transparent for chip rows), `text[80]` color, mono or sans font.
124+
125+
### Button / LinkButton
126+
127+
Lives in `@/design-system/components`. Two color modes: `primary` (deep ink fill, white text) and `secondary` (transparent fill, ink text + 1px border). `variant="contained"` is what partner pages use.
128+
129+
### Avatar
130+
131+
`PartnerAvatar` is a deterministic generated mark from name + slug. Used as fallback when `profilePictureUrl` is missing. The real photo overlays it at 120px circle on the detail page, 56px on the list card.
132+
133+
## Motion
134+
135+
- Hover transitions: 250ms, ease-out (cubic-bezier curve in `PartnerCard`: `0.25s ease`).
136+
- Card entrance: 700ms cubic-bezier `0.22, 1, 0.36, 1` (ease-out-quart), 90ms stagger per index.
137+
- All motion respects `@media (prefers-reduced-motion: reduce)` — animations stop, hover translate disabled.
138+
- **No bounce, no elastic, no parallax.** Editorial restraint.
139+
140+
## Iconography
141+
142+
`@tabler/icons-react`, 14–16px on body-level chips, 18–24px on buttons. Always `aria-hidden="true"` when decorative. Stroke width `2` (default).
143+
144+
## Accessibility Defaults
145+
146+
- Focus ring: `outline: 2px solid theme.colors.primary.text[100]; outline-offset: 4px` (already used on the card link).
147+
- Touch target ≥ 40×40px on mobile.
148+
- `aria-label` on icon-only buttons, `aria-labelledby` on sectioned regions.
149+
- All `<a target="_blank">` includes `rel="noopener noreferrer"`.
150+
- Color is never the sole carrier of meaning. The money pills carry both an icon and a text label.
151+
152+
## Anti-patterns (project-specific)
153+
154+
In addition to the impeccable shared absolute bans:
155+
156+
- **Do not use the brand accent colors (blue/pink/yellow/green) on partner pages** unless we have a stronger reason than "to add color".
157+
- **No skeuomorphic shadows on cards.** The hover shadow is `0 12px 32px -16px rgba(0,0,0,0.18)` — that's the ceiling.
158+
- **No gradients on anything.** Including text, borders, and backgrounds.
159+
- **No floating "Trusted by" logo bars** on partner pages.

PRODUCT.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Twenty Website — Product & Brand Context
2+
3+
> Strategic context for design work on the Twenty marketing site (`packages/twenty-website`). Loaded by every `impeccable` invocation.
4+
5+
## Register
6+
7+
**Brand.** The marketing site is a public-facing surface where the design itself is part of the credibility argument. Prospects evaluate Twenty partly by how the site feels. The product app (`packages/twenty-front`) is a separate product-register surface, governed elsewhere.
8+
9+
## Users & Purpose
10+
11+
The primary audience varies by route, but the working assumption for partner-related pages is:
12+
13+
- **Who:** A budget-holding decision maker (founder, RevOps lead, or COO) shopping for a CRM implementation partner. Already on Twenty's site, evaluating a shortlist of partners.
14+
- **Context:** Doing a side-by-side comparison across 2–5 candidates over a single browsing session. Will spend 30–90 seconds on each profile before deciding whether to book a call.
15+
- **Decision being made:** "Is this partner credible, the right size, the right specialty, and within budget? Do I trust them enough to commit 30 minutes to a discovery call?"
16+
17+
What the partner pages must do, in priority order:
18+
1. Communicate credibility (real firm, real person, real work).
19+
2. Surface fit signals fast (skills, region, languages, deployment expertise, budget range).
20+
3. Give the visitor a confident "next step" affordance (book a call or vet via LinkedIn) without pressure.
21+
22+
## Desired Outcome
23+
24+
The redesign should make `/partners/profile/[slug]` feel like a *thoughtfully curated profile of a top-tier partner*, not a generic templated card. A visitor should leave thinking "this firm is serious" even if they don't book a call this session.
25+
26+
Specifically:
27+
- **Confidence over information density.** A short, well-typeset profile beats a packed-but-busy one.
28+
- **Editorial restraint.** White space, deliberate type hierarchy, and a few well-chosen details say more than dozens of small components.
29+
- **Quiet conviction.** No hype copy, no growth-hack patterns, no "Trusted by" logo strips. The partner's own work and intro speak for themselves.
30+
31+
## Brand Personality
32+
33+
**Editorial · Founder-led · Considered.**
34+
35+
The site reads like a thoughtful indie publication, not a SaaS landing page. Serif headlines, plenty of whitespace, deliberate typographic rhythm. Quietly opinionated — Twenty has a point of view about CRM (open-source, customizable, well-designed) and the site reflects that without shouting.
36+
37+
Tonal anchors:
38+
- Stripe's documentation for clarity, Linear's marketing for restraint, an editorial print magazine for typography choices.
39+
40+
## Anti-references
41+
42+
**Reject these patterns. They make the work read as generic AI / generic SaaS:**
43+
44+
- **Generic SaaS landing.** Big-number heroes, identical icon-grid cards, gradient text, navy + lime accent color schemes, "supercharge your workflow" language.
45+
- **Corporate enterprise tone.** Stock photos of diverse handshakes. "Trusted by Fortune 500" logo strips as the primary credibility move. Trust-badge bars.
46+
- **Bento templates.** Repetitive same-size cards. Vercel-style scroll-pin animations on every section.
47+
- **Side-stripe borders, gradient text, glassmorphism, hero-metric templates, identical card grids** — see impeccable's shared absolute bans.
48+
49+
## Strategic Design Principles
50+
51+
1. **Typography carries the design.** The brand has a serif/sans/mono trio. Hierarchy is set by scale + weight contrast, not by color or borders.
52+
2. **Restrained palette.** Tinted neutrals (black/white via CSS variables, with alpha-tone variants for text and borders) carry 90%+ of the surface. Accent color used sparingly when it appears at all.
53+
3. **Whitespace is a feature.** Tight cards feel cheap. Pages should breathe.
54+
4. **Asymmetry over grid.** A 12-col bento is the wrong shape for a profile page. Use asymmetric two-column layouts where one column does heavy lifting.
55+
5. **One opinionated detail per page.** Each surface should have one moment of editorial conviction (a typographic flourish, a precise micro-interaction, a deliberate space) rather than five generic flourishes.
56+
57+
## Accessibility
58+
59+
**WCAG AA + keyboard + screen reader baseline:**
60+
61+
- All interactive elements reachable by keyboard, focus visible (`outline: 2px solid`, not just color shift).
62+
- Semantic landmarks: `<header>`, `<main>`, `<nav>`, `<section aria-labelledby=…>`, headings in order.
63+
- All images with informational content have alt text. Decorative icons have `aria-hidden="true"`.
64+
- Body text ≥ 4.5:1 contrast; large text (≥18pt or 14pt bold) ≥ 3:1.
65+
- Respect `prefers-reduced-motion`. Animations stop, don't slow.
66+
- Forms have explicit labels. Errors are announced.
67+
68+
## Tech & Constraints
69+
70+
- Next.js 16 app router (Server Components by default, `'use client'` for interactivity).
71+
- Linaria styled-components (`@linaria/react`) for zero-runtime CSS-in-JS.
72+
- Lingui (`@lingui/react`) for i18n; never hardcode user-visible strings.
73+
- Theme tokens in `packages/twenty-website/src/theme/`. Colors are CSS variables resolved to OKLCH-tinted neutrals.
74+
- `@tabler/icons-react` for iconography (no Heroicons, no custom SVGs unless purposeful).
75+
- `@radix-ui/react-*` for primitives (Popover etc) where headless behavior is needed.
76+
77+
## Out of Scope for This File
78+
79+
- Detailed visual tokens (colors, type scale, motion specs) live in `DESIGN.md`.
80+
- Per-page IA decisions live in shape briefs (`docs/superpowers/specs/`).

packages/twenty-website/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@lingui/core": "^5.1.2",
2323
"@lingui/react": "^5.1.2",
2424
"@lottiefiles/dotlottie-react": "^0.18.10",
25+
"@radix-ui/react-popover": "^1.1.15",
2526
"@tabler/icons-react": "^3.41.1",
2627
"@wyw-in-js/babel-preset": "^0.8.1",
2728
"framer-motion": "^11.18.0",

packages/twenty-website/src/app/[locale]/partners/list/MarketplaceClient.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,18 @@ const FilterBarInner = styled(Container)`
3434

3535
type MarketplaceClientProps = {
3636
partners: readonly MarketplacePartner[];
37+
locale: string;
3738
};
3839

39-
export function MarketplaceClient({ partners }: MarketplaceClientProps) {
40+
export function MarketplaceClient({
41+
partners,
42+
locale,
43+
}: MarketplaceClientProps) {
4044
const {
4145
criteria,
4246
toggleRegion,
4347
toggleLanguage,
44-
toggleDeployment,
48+
toggleCategory,
4549
clearAll,
4650
hasAnyFilter,
4751
} = useFilterState();
@@ -62,15 +66,15 @@ export function MarketplaceClient({ partners }: MarketplaceClientProps) {
6266
hasAnyFilter={hasAnyFilter}
6367
onToggleRegion={toggleRegion}
6468
onToggleLanguage={toggleLanguage}
65-
onToggleDeployment={toggleDeployment}
69+
onToggleCategory={toggleCategory}
6670
onClearAll={clearAll}
6771
/>
6872
</FilterBarInner>
6973
</FilterBarOuter>
7074
{filteredPartners.length === 0 ? (
7175
<EmptyState onClearFilters={clearAll} />
7276
) : (
73-
<MarketplaceGrid partners={filteredPartners} />
77+
<MarketplaceGrid partners={filteredPartners} locale={locale} />
7478
)}
7579
</>
7680
);

packages/twenty-website/src/app/[locale]/partners/list/__tests__/PartnerCard.test.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,33 @@ const FIXTURE: MarketplacePartner = {
1717
name: 'Test Partner',
1818
introduction: 'A reliable partner for testing purposes.',
1919
calendarLink: 'https://calendly.com/test-partner',
20-
deploymentExpertise: ['CLOUD', 'SELF_HOST'],
20+
partnerScope: ['HOSTING', 'DEVELOPMENT'],
2121
region: ['EUROPE', 'US'],
2222
languagesSpoken: ['ENGLISH', 'FRENCH'],
23+
hourlyRateUsd: null,
24+
projectBudgetMinUsd: null,
25+
projectBudgetTypicalUsd: null,
26+
linkedinUrl: '',
27+
profilePictureUrl: '',
28+
city: '',
29+
country: '',
30+
skills: [],
2331
};
2432

2533
const renderCard = () =>
2634
renderToStaticMarkup(
2735
<I18nProvider i18n={i18n}>
28-
<PartnerCard partner={FIXTURE} index={0} />
36+
<PartnerCard partner={FIXTURE} index={0} locale="en" />
2937
</I18nProvider>,
3038
);
3139

3240
describe('PartnerCard', () => {
33-
it('renders the partner name as the article heading', () => {
41+
it('renders the partner name inside the detail-page link in the heading', () => {
3442
const html = renderCard();
35-
expect(html).toMatch(new RegExp(`<h3[^>]*>${FIXTURE.name}</h3>`, 'i'));
43+
expect(html).toMatch(
44+
new RegExp(`<h3[^>]*>\\s*<a[^>]*>${FIXTURE.name}</a>\\s*</h3>`, 'i'),
45+
);
46+
expect(html).toContain(`href="/en/partners/profile/${FIXTURE.slug}"`);
3647
});
3748

3849
it('renders the geo eyebrow with the first served region', () => {
@@ -50,7 +61,7 @@ describe('PartnerCard', () => {
5061
const expectedChipCount =
5162
FIXTURE.region.length +
5263
FIXTURE.languagesSpoken.length +
53-
FIXTURE.deploymentExpertise.length;
64+
FIXTURE.partnerScope.length;
5465
const liMatches = html.match(/<li[^>]*>/g) ?? [];
5566
expect(liMatches.length).toBe(expectedChipCount);
5667
});
@@ -74,9 +85,10 @@ describe('PartnerCard', () => {
7485
<PartnerCard
7586
partner={{ ...FIXTURE, calendarLink: unsafeLink }}
7687
index={0}
88+
locale="en"
7789
/>
7890
</I18nProvider>,
7991
);
80-
expect(html).not.toContain('href=');
92+
expect(html).not.toContain(`href="${unsafeLink}"`);
8193
});
8294
});

0 commit comments

Comments
 (0)