Skip to content

Commit c327621

Browse files
fix(landing): regroup enterprise feature cards into 4/4/2/2 at two-column band
Merge the four enterprise feature sections into one EnterpriseFeatureGrid so the 640-1023px two-column layout fills 4/4/2/2 with no orphan empty cell; lg+ and <sm layouts are unchanged via sm:max-lg order utilities. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d5746c8 commit c327621

7 files changed

Lines changed: 219 additions & 58 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { SolutionsCard } from './solutions-card'
2+
export { SolutionsCardRowHeader } from './solutions-card-row-header'
23
export { SolutionsPillCta } from './solutions-pill-cta'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { SolutionsCardRowHeader } from './solutions-card-row-header'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { cn } from '@sim/emcn'
2+
import { SolutionsPillCta } from '@/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-pill-cta'
3+
import {
4+
SOLUTIONS_SPACING,
5+
SOLUTIONS_TEXT_MEASURE,
6+
} from '@/app/(landing)/components/solutions-page/constants'
7+
import type { SolutionsCardRowConfig } from '@/app/(landing)/components/solutions-page/types'
8+
9+
/**
10+
* The header block of a card row - an `<h2>` title, a body-color subtitle, and
11+
* a single pill CTA, stacked with named spacing constants. Extracted from
12+
* {@link SolutionsCardRow} so layouts that place row headers inside a shared
13+
* grid (the enterprise feature grid) render the exact same header chrome.
14+
*
15+
* Only the row header stack can opt into centered text; the `feature` variant
16+
* is the tighter, smaller treatment used by feature-tile pages.
17+
*/
18+
19+
interface SolutionsCardRowHeaderProps {
20+
row: SolutionsCardRowConfig
21+
/** Stable id wiring the `<h2>` into the page outline. */
22+
headingId: string
23+
/** Header stack alignment. Defaults to the original left-aligned layout. */
24+
align?: 'left' | 'center'
25+
/** Header typography treatment. Defaults to the original larger solutions header. */
26+
variant?: 'standard' | 'feature'
27+
}
28+
29+
export function SolutionsCardRowHeader({
30+
row,
31+
headingId,
32+
align = 'left',
33+
variant = 'standard',
34+
}: SolutionsCardRowHeaderProps) {
35+
const centered = align === 'center'
36+
const featureHeader = variant === 'feature'
37+
38+
return (
39+
<div
40+
className={cn(
41+
'flex flex-col',
42+
centered ? 'items-center text-center' : 'items-start text-left',
43+
featureHeader ? 'gap-3' : SOLUTIONS_SPACING.cardRowHeaderStack
44+
)}
45+
>
46+
<h2
47+
id={headingId}
48+
className={cn(
49+
'text-balance text-[var(--text-primary)] leading-[1.3]',
50+
featureHeader
51+
? 'max-w-[540px] font-medium text-[22px] max-sm:text-[20px]'
52+
: 'max-w-[760px] text-[32px] max-sm:text-[24px]'
53+
)}
54+
>
55+
{row.title}
56+
</h2>
57+
<p
58+
className={cn(
59+
featureHeader ? 'w-full min-w-0 max-w-[48ch]' : SOLUTIONS_TEXT_MEASURE.rowSubtitle,
60+
'text-pretty',
61+
featureHeader
62+
? 'text-[15px] text-[var(--text-muted)] leading-[1.6]'
63+
: 'text-[20px] text-[var(--text-body)] leading-[1.5]'
64+
)}
65+
>
66+
{row.subtitle}
67+
</p>
68+
<div
69+
className={
70+
featureHeader
71+
? SOLUTIONS_SPACING.cardRowHeaderCtaGapFeature
72+
: SOLUTIONS_SPACING.cardRowHeaderCtaGap
73+
}
74+
>
75+
<SolutionsPillCta cta={row.cta} />
76+
</div>
77+
</div>
78+
)
79+
}

apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/solutions-card-row.tsx

Lines changed: 8 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { cn } from '@sim/emcn'
22
import {
33
SolutionsCard,
4-
SolutionsPillCta,
4+
SolutionsCardRowHeader,
55
} from '@/app/(landing)/components/solutions-page/components/solutions-card-row/components'
6-
import {
7-
SOLUTIONS_SPACING,
8-
SOLUTIONS_TEXT_MEASURE,
9-
} from '@/app/(landing)/components/solutions-page/constants'
6+
import { SOLUTIONS_SPACING } from '@/app/(landing)/components/solutions-page/constants'
107
import type { SolutionsCardRowConfig } from '@/app/(landing)/components/solutions-page/types'
118

129
/**
@@ -46,54 +43,19 @@ export function SolutionsCardRow({
4643
}: SolutionsCardRowProps) {
4744
const headingId = `solutions-row-${row.id}-heading`
4845
const gridCols = GRID_COLS[row.cards.length] ?? GRID_COLS[3]
49-
const centered = align === 'center'
50-
const featureHeader = headerVariant === 'feature'
5146

5247
return (
5348
<section
5449
id={`solutions-row-${row.id}`}
5550
aria-labelledby={headingId}
5651
className={cn('flex flex-col', SOLUTIONS_SPACING.cardRowHeaderToGrid)}
5752
>
58-
<div
59-
className={cn(
60-
'flex flex-col',
61-
centered ? 'items-center text-center' : 'items-start text-left',
62-
featureHeader ? 'gap-3' : SOLUTIONS_SPACING.cardRowHeaderStack
63-
)}
64-
>
65-
<h2
66-
id={headingId}
67-
className={cn(
68-
'text-balance text-[var(--text-primary)] leading-[1.3]',
69-
featureHeader
70-
? 'max-w-[540px] font-medium text-[22px] max-sm:text-[20px]'
71-
: 'max-w-[760px] text-[32px] max-sm:text-[24px]'
72-
)}
73-
>
74-
{row.title}
75-
</h2>
76-
<p
77-
className={cn(
78-
featureHeader ? 'w-full min-w-0 max-w-[48ch]' : SOLUTIONS_TEXT_MEASURE.rowSubtitle,
79-
'text-pretty',
80-
featureHeader
81-
? 'text-[15px] text-[var(--text-muted)] leading-[1.6]'
82-
: 'text-[20px] text-[var(--text-body)] leading-[1.5]'
83-
)}
84-
>
85-
{row.subtitle}
86-
</p>
87-
<div
88-
className={
89-
featureHeader
90-
? SOLUTIONS_SPACING.cardRowHeaderCtaGapFeature
91-
: SOLUTIONS_SPACING.cardRowHeaderCtaGap
92-
}
93-
>
94-
<SolutionsPillCta cta={row.cta} />
95-
</div>
96-
</div>
53+
<SolutionsCardRowHeader
54+
row={row}
55+
headingId={headingId}
56+
align={align}
57+
variant={headerVariant}
58+
/>
9759

9860
<div
9961
className={cn(
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { cn } from '@sim/emcn'
2+
import {
3+
SolutionsCard,
4+
SolutionsCardRowHeader,
5+
} from '@/app/(landing)/components/solutions-page/components/solutions-card-row/components'
6+
import { SOLUTIONS_SPACING } from '@/app/(landing)/components/solutions-page/constants'
7+
import type { SolutionsCardRowConfig } from '@/app/(landing)/components/solutions-page/types'
8+
9+
/**
10+
* The enterprise feature sections rendered as ONE shared CSS grid so cards can
11+
* reflow across section boundaries in the two-column band (`sm`..`lg`,
12+
* 640-1023px). Separate per-section grids (what {@link SolutionsCardRow}
13+
* renders) leave an orphan cell there: each 3-card section breaks 2 + 1.
14+
*
15+
* Layout per breakpoint:
16+
* - `lg`+ (3 columns): source order - every header is followed by its own 3
17+
* cards, matching {@link SolutionsCardRow} exactly.
18+
* - `sm`..`lg` (2 columns): the `sm:max-lg:order-*` classes regroup the 12
19+
* cards into balanced blocks of 4 / 4 / 2 / 2 beneath the four headers, so
20+
* no grid cell is ever empty. Cards borrowed from the next section render
21+
* under the previous header in this band only.
22+
* - below `sm` (1 column): source order again - each header stacks above its
23+
* own 3 cards.
24+
*
25+
* Each section keeps its `<section aria-labelledby>` landmark via
26+
* `display: contents`, so its header and cards participate directly in the
27+
* outer grid while the document outline (H2 -> H3) and anchor ids stay
28+
* identical to the `SolutionsCardRow` markup.
29+
*
30+
* Vertical rhythm is reproduced from the flex-column layout it replaces: the
31+
* grid's own `gap-8` (32px) supplies the inter-card gap, and headers carry
32+
* margins that top the gap up to the original values - `mb-4` lands the
33+
* header->cards distance at 48px (`cardRowHeaderToGrid`), and the top margins
34+
* land the section->section distance at 120/88/64px (`sectionRhythm`).
35+
*/
36+
37+
interface EnterpriseFeatureGridProps {
38+
/** The four 3-card feature rows, in source order. */
39+
rows: SolutionsCardRowConfig[]
40+
}
41+
42+
/**
43+
* Header top margins recreating `LANDING_SECTION_RHYTHM` (120/88/64px) on top
44+
* of the grid's 32px row gap; `mb-4` recreates `cardRowHeaderToGrid` (48px).
45+
*/
46+
const HEADER_RHYTHM = 'mt-[88px] mb-4 max-lg:mt-14 max-sm:mt-8'
47+
const FIRST_HEADER_RHYTHM = 'mb-4'
48+
49+
/** Two-column-band ordering for the four headers (positions 1, 6, 11, 14). */
50+
const TABLET_HEADER_ORDER = [
51+
'sm:max-lg:order-1',
52+
'sm:max-lg:order-6',
53+
'sm:max-lg:order-11',
54+
'sm:max-lg:order-[14]',
55+
] as const
56+
57+
/**
58+
* Two-column-band ordering for the 12 cards (flat index = rowIndex * 3 +
59+
* cardIndex), interleaved with {@link TABLET_HEADER_ORDER} to produce the
60+
* 4 / 4 / 2 / 2 grouping.
61+
*/
62+
const TABLET_CARD_ORDER = [
63+
'sm:max-lg:order-2',
64+
'sm:max-lg:order-3',
65+
'sm:max-lg:order-4',
66+
'sm:max-lg:order-5',
67+
'sm:max-lg:order-7',
68+
'sm:max-lg:order-8',
69+
'sm:max-lg:order-9',
70+
'sm:max-lg:order-10',
71+
'sm:max-lg:order-12',
72+
'sm:max-lg:order-[13]',
73+
'sm:max-lg:order-[15]',
74+
'sm:max-lg:order-[16]',
75+
] as const
76+
77+
export function EnterpriseFeatureGrid({ rows }: EnterpriseFeatureGridProps) {
78+
return (
79+
<div
80+
className={cn(
81+
'grid grid-cols-3 max-sm:grid-cols-1 max-lg:grid-cols-2',
82+
SOLUTIONS_SPACING.cardGridGap
83+
)}
84+
>
85+
{rows.map((row, rowIndex) => {
86+
const headingId = `solutions-row-${row.id}-heading`
87+
88+
return (
89+
<section
90+
key={row.id}
91+
id={`solutions-row-${row.id}`}
92+
aria-labelledby={headingId}
93+
className='contents'
94+
>
95+
<div
96+
className={cn(
97+
'col-span-full',
98+
rowIndex === 0 ? FIRST_HEADER_RHYTHM : HEADER_RHYTHM,
99+
TABLET_HEADER_ORDER[rowIndex]
100+
)}
101+
>
102+
<SolutionsCardRowHeader row={row} headingId={headingId} variant='feature' />
103+
</div>
104+
{row.cards.map((card, cardIndex) => (
105+
<div
106+
key={`${row.id}-${card.title}`}
107+
className={cn('min-w-0', TABLET_CARD_ORDER[rowIndex * 3 + cardIndex])}
108+
>
109+
<SolutionsCard
110+
card={card}
111+
headingId={`solutions-row-${row.id}-card-${cardIndex}-heading`}
112+
variant='featureTile'
113+
/>
114+
</div>
115+
))}
116+
</section>
117+
)
118+
})}
119+
</div>
120+
)
121+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { EnterpriseFeatureGrid } from './enterprise-feature-grid'

apps/sim/app/(landing)/enterprise/enterprise.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import Image from 'next/image'
33
import { Cta } from '@/app/(landing)/components/cta/cta'
44
import { LANDING_CONTENT_WIDTH, LANDING_GUTTER } from '@/app/(landing)/components/landing-layout'
55
import {
6-
SolutionsCardRow,
76
SolutionsHero,
87
SolutionsLogosRow,
98
SolutionsStructuredData,
109
} from '@/app/(landing)/components/solutions-page/components'
1110
import { SOLUTIONS_SPACING } from '@/app/(landing)/components/solutions-page/constants'
1211
import type { SolutionsPageConfig } from '@/app/(landing)/components/solutions-page/types'
12+
import { EnterpriseFeatureGrid } from '@/app/(landing)/enterprise/components/enterprise-feature-grid'
1313
import { EnterprisePlatformLoop } from '@/app/(landing)/enterprise/components/enterprise-platform-loop'
1414
import {
1515
AccessControlGraphic,
@@ -32,9 +32,12 @@ import {
3232
*
3333
* Structurally it mirrors {@link SolutionsPage} (hero → logos → card rows) but
3434
* composes its own `<main>` so it can append the shared homepage CTA that
35-
* `SolutionsPage` does not render. The shared `SOLUTIONS_SPACING` constants own
36-
* the enterprise content gutter and inter-section rhythm, while the homepage
37-
* {@link Cta} owns the closing conversion band.
35+
* `SolutionsPage` does not render. The four feature rows render through
36+
* {@link EnterpriseFeatureGrid} - one shared grid that regroups the 12 cards
37+
* into 4/4/2/2 in the two-column band so no section leaves an orphan cell.
38+
* The shared `SOLUTIONS_SPACING` constants own the enterprise content gutter
39+
* and inter-section rhythm, while the homepage {@link Cta} owns the closing
40+
* conversion band.
3841
*
3942
* The strict heading outline is H1 (hero) → H2 (each card row + the CTA) → H3
4043
* (each card), never skipped. Server Component; the interactive leaves live in
@@ -202,14 +205,7 @@ export default function EnterprisePage() {
202205
)}
203206
>
204207
<SolutionsLogosRow />
205-
{ENTERPRISE_CONFIG.rows.map((row) => (
206-
<SolutionsCardRow
207-
key={row.id}
208-
row={row}
209-
cardVariant='featureTile'
210-
headerVariant='feature'
211-
/>
212-
))}
208+
<EnterpriseFeatureGrid rows={ENTERPRISE_CONFIG.rows} />
213209
</div>
214210

215211
<Cta />

0 commit comments

Comments
 (0)