GeezerVison is showing github.com — fix(landing): regroup enterprise feature cards into 4/4/2/2 at two-co… · simstudioai/sim@c327621
File treeExpand file treeCollapse file tree Open diff view settings components/solutions-page/components/solutions-card-row solutions-card-row-header
components/enterprise-feature-grid
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboard +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | export { SolutionsCard } from './solutions-card' | | 2 | +export { SolutionsCardRowHeader } from './solutions-card-row-header' | 2 | 3 | export { SolutionsPillCta } from './solutions-pill-cta' |
Collapse file Copy file name to clipboard +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +export { SolutionsCardRowHeader } from './solutions-card-row-header' |
Collapse file Copy file name to clipboard +79Lines changed: 79 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +} |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/solutions-card-row.tsx +8-46Lines changed: 8 additions & 46 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | import { cn } from '@sim/emcn' | 2 | 2 | import { | 3 | 3 | SolutionsCard, | 4 | | - SolutionsPillCta, | | 4 | + SolutionsCardRowHeader, | 5 | 5 | } 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' | 10 | 7 | import type { SolutionsCardRowConfig } from '@/app/(landing)/components/solutions-page/types' | 11 | 8 |
| 12 | 9 | /** | @@ -46,54 +43,19 @@ export function SolutionsCardRow({ | 46 | 43 | }: SolutionsCardRowProps) { | 47 | 44 | const headingId = `solutions-row-${row.id}-heading` | 48 | 45 | const gridCols = GRID_COLS[row.cards.length] ?? GRID_COLS[3] | 49 | | - const centered = align === 'center' | 50 | | - const featureHeader = headerVariant === 'feature' | 51 | 46 |
| 52 | 47 | return ( | 53 | 48 | <section | 54 | 49 | id={`solutions-row-${row.id}`} | 55 | 50 | aria-labelledby={headingId} | 56 | 51 | className={cn('flex flex-col', SOLUTIONS_SPACING.cardRowHeaderToGrid)} | 57 | 52 | > | 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 | + /> | 97 | 59 |
| 98 | 60 | <div | 99 | 61 | className={cn( | |
Collapse file Copy file name to clipboard +121Lines changed: 121 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +} |
Collapse file Copy file name to clipboard +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +export { EnterpriseFeatureGrid } from './enterprise-feature-grid' |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/app/(landing)/enterprise/enterprise.tsx +8-12Lines changed: 8 additions & 12 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -3,13 +3,13 @@ import Image from 'next/image' | 3 | 3 | import { Cta } from '@/app/(landing)/components/cta/cta' | 4 | 4 | import { LANDING_CONTENT_WIDTH, LANDING_GUTTER } from '@/app/(landing)/components/landing-layout' | 5 | 5 | import { | 6 | | - SolutionsCardRow, | 7 | 6 | SolutionsHero, | 8 | 7 | SolutionsLogosRow, | 9 | 8 | SolutionsStructuredData, | 10 | 9 | } from '@/app/(landing)/components/solutions-page/components' | 11 | 10 | import { SOLUTIONS_SPACING } from '@/app/(landing)/components/solutions-page/constants' | 12 | 11 | import type { SolutionsPageConfig } from '@/app/(landing)/components/solutions-page/types' | | 12 | +import { EnterpriseFeatureGrid } from '@/app/(landing)/enterprise/components/enterprise-feature-grid' | 13 | 13 | import { EnterprisePlatformLoop } from '@/app/(landing)/enterprise/components/enterprise-platform-loop' | 14 | 14 | import { | 15 | 15 | AccessControlGraphic, | @@ -32,9 +32,12 @@ import { | 32 | 32 | * | 33 | 33 | * Structurally it mirrors {@link SolutionsPage} (hero → logos → card rows) but | 34 | 34 | * 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. | 38 | 41 | * | 39 | 42 | * The strict heading outline is H1 (hero) → H2 (each card row + the CTA) → H3 | 40 | 43 | * (each card), never skipped. Server Component; the interactive leaves live in | @@ -202,14 +205,7 @@ export default function EnterprisePage() { | 202 | 205 | )} | 203 | 206 | > | 204 | 207 | <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} /> | 213 | 209 | </div> | 214 | 210 |
| 215 | 211 | <Cta /> | |
|