chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export { PlatformCardRow } from './platform-card-row'
|
||||
export { PlatformHero } from './platform-hero'
|
||||
export { PlatformLogosRow } from './platform-logos-row'
|
||||
export { PlatformStructuredData } from './platform-structured-data'
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export { PlatformCard } from './platform-card'
|
||||
export { PlatformPillCta } from './platform-pill-cta'
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { PlatformCard } from './platform-card'
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { cn } from '@sim/emcn'
|
||||
import { PlatformVisualFrame } from '@/app/(landing)/components/platform-page/components/platform-visual-frame'
|
||||
import { PLATFORM_SPACING } from '@/app/(landing)/components/platform-page/constants'
|
||||
import type { PlatformCardConfig } from '@/app/(landing)/components/platform-page/types'
|
||||
|
||||
/**
|
||||
* A single platform card - an `<article>` with an `<h3>` title, a body-color
|
||||
* description, and a reserved visual panel beneath. Text sits directly on the
|
||||
* canvas (matching the hero and every other landing section); only the visual
|
||||
* carries the `--surface-2` panel chrome, so the product mock reads as the one
|
||||
* elevated surface and never blends into a competing card fill.
|
||||
*
|
||||
* The card owns the gap between its text and visual (`cardTextToVisual`) and the
|
||||
* title→description stack (`cardTextStack`) - both from named spacing constants.
|
||||
* The text block grows (`flex-1`) so the visual pins to the bottom of the
|
||||
* grid-stretched cell: every card's visual aligns on one baseline regardless of
|
||||
* description length. The visual lands in a fixed-height {@link PlatformVisualFrame}
|
||||
* so the row stays uniform and CLS is zero.
|
||||
*
|
||||
* A content unit only: it accepts copy and a visual node, never any layout knob.
|
||||
*/
|
||||
|
||||
interface PlatformCardProps {
|
||||
card: PlatformCardConfig
|
||||
/** Stable id wiring the `<h3>` into the page outline. */
|
||||
headingId: string
|
||||
}
|
||||
|
||||
export function PlatformCard({ card, headingId }: PlatformCardProps) {
|
||||
return (
|
||||
<article className={cn('flex h-full flex-col', PLATFORM_SPACING.cardTextToVisual)}>
|
||||
<div className={cn('flex flex-1 flex-col', PLATFORM_SPACING.cardTextStack)}>
|
||||
<h3 id={headingId} className='text-[18px] text-[var(--text-primary)] leading-[1.3]'>
|
||||
{card.title}
|
||||
</h3>
|
||||
<p className='text-[15px] text-[var(--text-body)] leading-[1.5]'>{card.description}</p>
|
||||
</div>
|
||||
|
||||
<PlatformVisualFrame size='card'>{card.visual}</PlatformVisualFrame>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { PlatformPillCta } from './platform-pill-cta'
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import type { PlatformPillCta as PlatformPillCtaConfig } from '@/app/(landing)/components/platform-page/types'
|
||||
|
||||
/**
|
||||
* The card-row pill CTA - a single primary `ChipLink` with a trailing arrow,
|
||||
* matching the reference image's "Learn about automations →". The chip owns its
|
||||
* own chrome and spacing; this component only wires the label, the href, and the
|
||||
* arrow icon, exposing no layout knobs.
|
||||
*
|
||||
* Client leaf: `ChipLink` is a Client Component and its `rightIcon` is a
|
||||
* component reference (`ArrowRight`), which cannot cross the server→client
|
||||
* boundary as a prop - so the icon must be wired from client code, exactly as the
|
||||
* navbar's `NavMenuChip` does. The props it receives ({@link PlatformPillCtaConfig})
|
||||
* are plain serializable data, so the surrounding layout stays Server Components.
|
||||
*
|
||||
* Link safety: an external href (absolute `http(s)://`) renders with
|
||||
* `rel='noopener noreferrer'` and `target='_blank'`; an internal href routes
|
||||
* through the Next `<Link>` that `ChipLink` is built on - so every link is
|
||||
* crawlable and safe with no per-page ceremony.
|
||||
*/
|
||||
|
||||
interface PlatformPillCtaProps {
|
||||
cta: PlatformPillCtaConfig
|
||||
}
|
||||
|
||||
/** Returns true for absolute external URLs (http/https), which need rel/target hardening. */
|
||||
function isExternalHref(href: string): boolean {
|
||||
return /^https?:\/\//.test(href)
|
||||
}
|
||||
|
||||
export function PlatformPillCta({ cta }: PlatformPillCtaProps) {
|
||||
const external = isExternalHref(cta.href)
|
||||
|
||||
return (
|
||||
<ChipLink
|
||||
variant='primary'
|
||||
href={cta.href}
|
||||
rightIcon={ArrowRight}
|
||||
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||||
>
|
||||
{cta.label}
|
||||
</ChipLink>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { PlatformCardRow } from './platform-card-row'
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { cn } from '@sim/emcn'
|
||||
import {
|
||||
PlatformCard,
|
||||
PlatformPillCta,
|
||||
} from '@/app/(landing)/components/platform-page/components/platform-card-row/components'
|
||||
import { PLATFORM_SPACING } from '@/app/(landing)/components/platform-page/constants'
|
||||
import type { PlatformCardRowConfig } from '@/app/(landing)/components/platform-page/types'
|
||||
|
||||
/**
|
||||
* A card row - the core repeating unit of a platform page. A header block (an
|
||||
* `<h2>` title, a body-color subtitle, and a single pill CTA) sits above a grid
|
||||
* of cards. The grid column count is derived from `cards.length` - 3 cards render
|
||||
* `grid-cols-3`, 4 render `grid-cols-4` - so the page never specifies layout.
|
||||
*
|
||||
* Rendered as a labelled `<section>` for a clean, crawlable landmark; each card
|
||||
* is an `<article>` with an `<h3>`, keeping the strict H2 → H3 hierarchy. Every
|
||||
* gap (header sub-stack, header-to-grid, and inter-card) is owned by named
|
||||
* spacing constants; this component exposes no layout prop.
|
||||
*/
|
||||
|
||||
interface PlatformCardRowProps {
|
||||
row: PlatformCardRowConfig
|
||||
}
|
||||
|
||||
/** Maps a supported card count to its grid column class; anything else falls back to three-up. */
|
||||
const GRID_COLS: Record<number, string> = {
|
||||
3: 'grid-cols-3',
|
||||
4: 'grid-cols-4',
|
||||
}
|
||||
|
||||
export function PlatformCardRow({ row }: PlatformCardRowProps) {
|
||||
const headingId = `platform-row-${row.id}-heading`
|
||||
const gridCols = GRID_COLS[row.cards.length] ?? GRID_COLS[3]
|
||||
|
||||
return (
|
||||
<section
|
||||
id={`platform-row-${row.id}`}
|
||||
aria-labelledby={headingId}
|
||||
className={cn('flex flex-col', PLATFORM_SPACING.cardRowHeaderToGrid)}
|
||||
>
|
||||
<div className={cn('flex flex-col items-start', PLATFORM_SPACING.cardRowHeaderStack)}>
|
||||
<h2
|
||||
id={headingId}
|
||||
className='max-w-[760px] text-balance text-[32px] text-[var(--text-primary)] leading-[1.3] max-sm:text-[24px]'
|
||||
>
|
||||
{row.title}
|
||||
</h2>
|
||||
<p className='max-w-[640px] text-[20px] text-[var(--text-body)] leading-[1.5]'>
|
||||
{row.subtitle}
|
||||
</p>
|
||||
<PlatformPillCta cta={row.cta} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'grid',
|
||||
gridCols,
|
||||
'max-sm:grid-cols-1 max-md:grid-cols-2',
|
||||
PLATFORM_SPACING.cardGridGap
|
||||
)}
|
||||
>
|
||||
{row.cards.map((card, index) => (
|
||||
<PlatformCard
|
||||
key={`${row.id}-${card.title}`}
|
||||
card={card}
|
||||
headingId={`platform-row-${row.id}-card-${index}-heading`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { PlatformHero } from './platform-hero'
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { cn } from '@sim/emcn'
|
||||
import { HeroCta } from '@/app/(landing)/components/hero-cta'
|
||||
import { LANDING_HERO_CTA_GAP } from '@/app/(landing)/components/landing-layout'
|
||||
import { PlatformVisualFrame } from '@/app/(landing)/components/platform-page/components/platform-visual-frame'
|
||||
import { PLATFORM_SPACING } from '@/app/(landing)/components/platform-page/constants'
|
||||
import type { PlatformHeroConfig } from '@/app/(landing)/components/platform-page/types'
|
||||
|
||||
/**
|
||||
* Platform hero - the only `<h1>` on a platform page. Left-aligned header copy
|
||||
* (headline + supporting description) sits above the same CTA as the landing
|
||||
* hero ({@link HeroCta}, the single source of truth), then a full-width platform
|
||||
* visual underneath.
|
||||
*
|
||||
* The header column and the visual are stacked in one flex column; the header's
|
||||
* own sub-stack (headline → description → CTA) and the gap down to the visual are
|
||||
* both owned by named spacing constants, so a consumer page passes only copy and
|
||||
* a visual node - never any spacing. The visual renders into a reserved-aspect
|
||||
* {@link PlatformVisualFrame} (CLS = 0).
|
||||
*
|
||||
* Carries the page's sr-only ~50-word product summary for AI citation (GEO). The
|
||||
* section's horizontal gutter is owned by `PlatformPage`; this component sets none.
|
||||
*/
|
||||
|
||||
interface PlatformHeroProps {
|
||||
hero: PlatformHeroConfig
|
||||
}
|
||||
|
||||
export function PlatformHero({ hero }: PlatformHeroProps) {
|
||||
return (
|
||||
<section
|
||||
id='platform-hero'
|
||||
aria-labelledby='platform-hero-heading'
|
||||
className={cn(
|
||||
'flex flex-col',
|
||||
PLATFORM_SPACING.heroTopPadding,
|
||||
PLATFORM_SPACING.heroToVisual
|
||||
)}
|
||||
>
|
||||
<p className='sr-only'>{hero.summary}</p>
|
||||
|
||||
<div className={cn('flex flex-col items-start text-left', PLATFORM_SPACING.heroStack)}>
|
||||
<h1
|
||||
id='platform-hero-heading'
|
||||
className='max-w-[900px] text-balance text-[48px] text-[var(--text-primary)] leading-[1.1] max-sm:text-[32px] max-xl:text-[40px]'
|
||||
>
|
||||
{hero.heading}
|
||||
</h1>
|
||||
|
||||
<p className='max-w-[640px] text-[20px] text-[var(--text-body)] leading-[1.5]'>
|
||||
{hero.description}
|
||||
</p>
|
||||
|
||||
<div className={cn('max-sm:w-full', LANDING_HERO_CTA_GAP)}>
|
||||
<HeroCta />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PlatformVisualFrame size='hero'>{hero.visual}</PlatformVisualFrame>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { PlatformLogosRow } from './platform-logos-row'
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { Logos } from '@/app/(landing)/components/logos'
|
||||
|
||||
/**
|
||||
* Platform logos row - the same customer wordmarks as the landing hero, in a
|
||||
* single horizontally-centered row at the shared `gap-x-24` rhythm (owned by the
|
||||
* shared {@link Logos} component, `row` layout). Takes no props and exposes no
|
||||
* spacing: its horizontal gutter comes from `PlatformPage` and its inter-section
|
||||
* spacing from the page's `<main>` gap.
|
||||
*
|
||||
* Wrapped as a labelled `<section>` so it is a discrete, crawlable landmark; the
|
||||
* heading is sr-only because the logos are a proof band rather than a content
|
||||
* section, but the H2 keeps the page's heading hierarchy intact. The section is
|
||||
* `relative` so the `sr-only` (`position: absolute`) heading is contained by it
|
||||
* rather than falling back to the document root - matching {@link Features}'s
|
||||
* `relative` wrapper for its own `sr-only` heading, and avoiding a phantom root
|
||||
* scrollbar (the heading's un-offset static position would otherwise inflate
|
||||
* `document.documentElement`'s scroll height by its own position in the page).
|
||||
*/
|
||||
export function PlatformLogosRow() {
|
||||
return (
|
||||
<section id='platform-logos' aria-labelledby='platform-logos-heading' className='relative'>
|
||||
<h2 id='platform-logos-heading' className='sr-only'>
|
||||
Companies building AI agents with Sim
|
||||
</h2>
|
||||
<div className='flex justify-center'>
|
||||
<Logos layout='row' />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { PlatformStructuredData } from './platform-structured-data'
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { SITE_URL } from '@/lib/core/utils/urls'
|
||||
import { JsonLd } from '@/app/(landing)/components/json-ld'
|
||||
import type { PlatformPageConfig } from '@/app/(landing)/components/platform-page/types'
|
||||
|
||||
/**
|
||||
* JSON-LD for a platform page - a `WebPage` (about a `WebApplication`) plus a
|
||||
* `BreadcrumbList`, rendered server-side before any visible content so crawlers
|
||||
* and AI answer engines read the structured data first.
|
||||
*
|
||||
* Everything is derived from the same {@link PlatformPageConfig} that drives the
|
||||
* visible sections, so the structured data can never drift from the page: the
|
||||
* `WebApplication.featureList` is the deduped set of card titles actually
|
||||
* rendered, the page name/description come from the hero, and the breadcrumb
|
||||
* from the module + path. The page author maintains zero schema by hand.
|
||||
*
|
||||
* Server Component; no client cost. Internal to the platform layout - emitted by
|
||||
* `PlatformPage`, never rendered by a consumer directly.
|
||||
*/
|
||||
|
||||
interface PlatformStructuredDataProps {
|
||||
config: PlatformPageConfig
|
||||
}
|
||||
|
||||
export function PlatformStructuredData({ config }: PlatformStructuredDataProps) {
|
||||
const { module, path, hero, rows } = config
|
||||
const url = `${SITE_URL}${path}`
|
||||
const featureList = Array.from(
|
||||
new Set(rows.flatMap((row) => row.cards.map((card) => card.title)))
|
||||
)
|
||||
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
{
|
||||
'@type': 'WebPage',
|
||||
'@id': `${url}#webpage`,
|
||||
url,
|
||||
name: hero.heading,
|
||||
description: hero.summary,
|
||||
isPartOf: { '@id': `${SITE_URL}#website` },
|
||||
about: { '@id': `${url}#application` },
|
||||
breadcrumb: { '@id': `${url}#breadcrumb` },
|
||||
inLanguage: 'en-US',
|
||||
},
|
||||
{
|
||||
'@type': 'BreadcrumbList',
|
||||
'@id': `${url}#breadcrumb`,
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL },
|
||||
{ '@type': 'ListItem', position: 2, name: module, item: url },
|
||||
],
|
||||
},
|
||||
{
|
||||
'@type': 'WebApplication',
|
||||
'@id': `${url}#application`,
|
||||
name: `Sim ${module}`,
|
||||
description: hero.summary,
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Web',
|
||||
url,
|
||||
featureList,
|
||||
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return <JsonLd data={jsonLd} />
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { PlatformVisualFrame } from './platform-visual-frame'
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@sim/emcn'
|
||||
import { PLATFORM_VISUAL } from '@/app/(landing)/components/platform-page/constants'
|
||||
|
||||
/**
|
||||
* The one escape hatch in the platform layout - a fixed-dimension frame that
|
||||
* holds a page-supplied visual `ReactNode`. The frame owns its chrome (the
|
||||
* hero-visual family: `--surface-2` fill, `--border-1` hairline, `rounded-lg`,
|
||||
* `overflow-hidden`) and, crucially, its dimensions: a `hero` frame reserves a
|
||||
* 16:9 aspect ratio, a `card` frame a fixed height. Because the size is reserved
|
||||
* before paint and the node fills `h-full w-full` inside, a dropped-in node can
|
||||
* neither shift surrounding layout (CLS = 0) nor change the frame's own padding.
|
||||
*
|
||||
* The frame is decorative chrome around product visuals, so it is `aria-hidden`;
|
||||
* the page's meaning lives in the adjacent headings and copy.
|
||||
*/
|
||||
|
||||
interface PlatformVisualFrameProps {
|
||||
/**
|
||||
* Reserved-dimension preset.
|
||||
* - `hero` - full-width 16:9 frame for the platform hero visual.
|
||||
* - `card` - fixed-height frame for a card's visual panel.
|
||||
*/
|
||||
size: 'hero' | 'card'
|
||||
/** The page-supplied visual island or static panel. Fills the frame; owns no chrome. */
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function PlatformVisualFrame({ size, children }: PlatformVisualFrameProps) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={cn(
|
||||
'w-full overflow-hidden rounded-lg border border-[var(--border-1)] bg-[var(--surface-2)]',
|
||||
size === 'hero' ? PLATFORM_VISUAL.heroAspect : PLATFORM_VISUAL.cardHeight
|
||||
)}
|
||||
>
|
||||
<div className='h-full w-full'>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Platform-layout spacing - the single source of truth for every gutter, gap,
|
||||
* inset, and reserved dimension used across the platform-page components.
|
||||
*
|
||||
* The padding fortress lives here. No platform component accepts a `className`,
|
||||
* `style`, or any layout-override prop, and none hard-codes a spacing value
|
||||
* inline. Every measurement a reviewer might want to audit - the horizontal
|
||||
* gutter, the inter-section rhythm, the card-grid gaps, the card stack, and the
|
||||
* fixed visual-slot dimensions - is named in this one file. To change spacing
|
||||
* you edit a constant here; a consumer page literally cannot reach it.
|
||||
*
|
||||
* All values are Tailwind class fragments (not raw numbers) so they compose
|
||||
* directly into `className` strings inside the components and stay legible.
|
||||
*/
|
||||
export const PLATFORM_SPACING = {
|
||||
/**
|
||||
* The one horizontal gutter, owned solely by `PlatformPage`. Matches the
|
||||
* navbar and landing hero exactly (`px-20 max-lg:px-8 max-sm:px-5`) so platform
|
||||
* content starts on the wordmark's vertical line at every breakpoint. Sections
|
||||
* and cards never set their own gutter.
|
||||
*/
|
||||
gutter: 'px-20 max-lg:px-8 max-sm:px-5',
|
||||
/**
|
||||
* Inter-section vertical rhythm - the gap of the `<main>` flex column that
|
||||
* `PlatformPage` owns. Sections carry no vertical margin/padding of their own,
|
||||
* so this is the only knob for the space between the hero, logos, and every
|
||||
* card row. Tightens on smaller screens in lockstep with the landing `<main>`
|
||||
* (`gap-[120px] max-lg:gap-[88px] max-sm:gap-16`).
|
||||
*/
|
||||
sectionRhythm: 'gap-[120px] max-lg:gap-[88px] max-sm:gap-16',
|
||||
/** Hero text top padding, matching the landing hero (`pt-[112px] max-sm:pt-12`). */
|
||||
heroTopPadding: 'pt-[112px] max-sm:pt-12',
|
||||
/** Vertical stack gap inside the hero header column (headline → description → CTA). */
|
||||
heroStack: 'gap-[22px]',
|
||||
/** Gap between the hero header column and the full-width hero visual beneath it. */
|
||||
heroToVisual: 'gap-12',
|
||||
/** Gap between a card row's header block and the card grid beneath it. */
|
||||
cardRowHeaderToGrid: 'gap-12',
|
||||
/** Vertical stack gap inside a card row's header (title → subtitle → CTA). */
|
||||
cardRowHeaderStack: 'gap-5',
|
||||
/** Gap between cards within a card-row grid (both axes). */
|
||||
cardGridGap: 'gap-8',
|
||||
/** Minimum gap between a card's text block and its visual panel. */
|
||||
cardTextToVisual: 'gap-5',
|
||||
/** Vertical stack gap inside a card's text block (title → description). */
|
||||
cardTextStack: 'gap-2',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Reserved fixed dimensions for the component-owned visual frames. A dropped-in
|
||||
* `ReactNode` renders into a frame of exactly these dimensions, so it can never
|
||||
* shift surrounding layout (CLS = 0) nor change its own frame padding. The node
|
||||
* fills `h-full w-full` inside; it owns nothing about the frame.
|
||||
*/
|
||||
export const PLATFORM_VISUAL = {
|
||||
/** Full-width hero visual aspect ratio - reserves height before paint. */
|
||||
heroAspect: 'aspect-[16/9]',
|
||||
/** Fixed height of a card's visual panel - uniform across every card. */
|
||||
cardHeight: 'h-[240px]',
|
||||
} as const
|
||||
@@ -0,0 +1,8 @@
|
||||
export { PlatformPage } from './platform-page'
|
||||
export type {
|
||||
PlatformCardConfig,
|
||||
PlatformCardRowConfig,
|
||||
PlatformHeroConfig,
|
||||
PlatformPageConfig,
|
||||
PlatformPillCta,
|
||||
} from './types'
|
||||
@@ -0,0 +1,59 @@
|
||||
import { cn } from '@sim/emcn'
|
||||
import {
|
||||
PlatformCardRow,
|
||||
PlatformHero,
|
||||
PlatformLogosRow,
|
||||
PlatformStructuredData,
|
||||
} from '@/app/(landing)/components/platform-page/components'
|
||||
import { PLATFORM_SPACING } from '@/app/(landing)/components/platform-page/constants'
|
||||
import type { PlatformPageConfig } from '@/app/(landing)/components/platform-page/types'
|
||||
|
||||
/**
|
||||
* The reusable platform-page content stack - the single component six routes
|
||||
* (Workflows, Tables, Files, Knowledge Base, Scheduled Tasks, Logs) consume with
|
||||
* near-zero ceremony. A route renders the shared shell and drops in one
|
||||
* `<PlatformPage config={…} />`.
|
||||
*
|
||||
* This component owns the entire `<main>`: the shared `max-w-[1460px]` content
|
||||
* column (centered with `mx-auto`, matching the navbar and landing sections), the
|
||||
* one horizontal gutter (`PLATFORM_SPACING.gutter`), and the inter-section
|
||||
* vertical rhythm (`PLATFORM_SPACING.sectionRhythm`, the `<main>` flex gap). The
|
||||
* hero, the logos row, and every card row carry no gutter and no inter-section
|
||||
* margin of their own, so spacing is uniform and unreachable from a consumer
|
||||
* page - the config is pure content (strings + `ReactNode` visual slots), with no
|
||||
* layout knob anywhere in its tree.
|
||||
*
|
||||
* The order is fixed: structured data first (before visible content, derived
|
||||
* from the same config so it never drifts) → platform hero (the page's only
|
||||
* `<h1>`) → centered logos row → the configured card rows in array order. The
|
||||
* heading outline is strict H1 → H2 (per card row) → H3 (per card), never
|
||||
* skipped. Server Component only - no client island lives here; the page
|
||||
* supplies its own islands through the `visual` slots in the config.
|
||||
*/
|
||||
|
||||
interface PlatformPageProps {
|
||||
/** The complete page content - identity, hero, and ordered card rows. */
|
||||
config: PlatformPageConfig
|
||||
}
|
||||
|
||||
export function PlatformPage({ config }: PlatformPageProps) {
|
||||
return (
|
||||
<>
|
||||
<PlatformStructuredData config={config} />
|
||||
<main
|
||||
id='main-content'
|
||||
className={cn(
|
||||
'mx-auto flex w-full max-w-[1460px] flex-col',
|
||||
PLATFORM_SPACING.sectionRhythm,
|
||||
PLATFORM_SPACING.gutter
|
||||
)}
|
||||
>
|
||||
<PlatformHero hero={config.hero} />
|
||||
<PlatformLogosRow />
|
||||
{config.rows.map((row) => (
|
||||
<PlatformCardRow key={row.id} row={row} />
|
||||
))}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Platform-page configuration - the entire content contract a route passes to
|
||||
* {@link PlatformPage}. Every field is content-only: strings for copy, `ReactNode`
|
||||
* exclusively for the designated visual/animation slots, and typed config arrays
|
||||
* for card rows. There is deliberately no `className`, `style`, width, height,
|
||||
* padding, margin, or any other layout knob anywhere in this tree - spacing
|
||||
* lives entirely inside the components (see `PLATFORM_SPACING`). A page describes
|
||||
* WHAT to show; the layout decides WHERE and with how much space.
|
||||
*/
|
||||
|
||||
/** A single pill call-to-action - label plus destination, used by card rows. */
|
||||
export interface PlatformPillCta {
|
||||
/** Visible link text. A trailing arrow is added by the component. */
|
||||
label: string
|
||||
/** Destination href. Internal hrefs render as Next `<Link>`; external as a safe anchor. */
|
||||
href: string
|
||||
}
|
||||
|
||||
/** The platform hero - header copy, the shared CTA, and a full-width visual. */
|
||||
export interface PlatformHeroConfig {
|
||||
/**
|
||||
* The page's single `<h1>`. Per the constitution it should name the module and
|
||||
* "Sim"/"AI workspace" (e.g. "Workflows - the visual builder in Sim, the AI workspace").
|
||||
*/
|
||||
heading: string
|
||||
/** Supporting description beneath the heading, in the body color. */
|
||||
description: string
|
||||
/**
|
||||
* ~50-word sr-only atomic summary for AI citation (GEO). Names "Sim" explicitly
|
||||
* and states what the module is, who it's for, and what it does.
|
||||
*/
|
||||
summary: string
|
||||
/**
|
||||
* The full-width hero visual - a page-supplied client island or static panel.
|
||||
* Renders into a component-owned frame with reserved aspect ratio (CLS = 0) and
|
||||
* is marked `aria-hidden`; it owns nothing about the frame's chrome or spacing.
|
||||
*/
|
||||
visual: ReactNode
|
||||
}
|
||||
|
||||
/** A single card - text plus a reserved visual panel. Rendered as an `<article>`. */
|
||||
export interface PlatformCardConfig {
|
||||
/** The card's `<h3>` title. */
|
||||
title: string
|
||||
/**
|
||||
* Supporting description beneath the title, in the body color. Self-contained
|
||||
* and names "Sim" - never "the platform" or a bare pronoun - so each card is an
|
||||
* independently quotable answer block.
|
||||
*/
|
||||
description: string
|
||||
/**
|
||||
* The card's visual/animation - a page-supplied node. Renders into a
|
||||
* component-owned, fixed-height frame (CLS = 0), marked `aria-hidden`. The card
|
||||
* owns the spacing around both the text and this frame.
|
||||
*/
|
||||
visual: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* A card row - the core repeating unit. A header (title + subtitle + CTA) above a
|
||||
* grid of 3 or 4 cards. The grid column count is derived from `cards.length`, so
|
||||
* the page never specifies layout.
|
||||
*/
|
||||
export interface PlatformCardRowConfig {
|
||||
/**
|
||||
* Stable section id for the `<section>` landmark and `aria-labelledby` wiring.
|
||||
* Must be unique within the page (e.g. `'build'`).
|
||||
*/
|
||||
id: string
|
||||
/** The row's `<h2>` title, in the headline color - reads as an answer to a user question. */
|
||||
title: string
|
||||
/** Supporting subtitle beneath the title, in the body color, naming "Sim". */
|
||||
subtitle: string
|
||||
/** The row's single pill CTA. */
|
||||
cta: PlatformPillCta
|
||||
/** The cards in this row - 3 or 4. The grid derives its columns from this length. */
|
||||
cards: PlatformCardConfig[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete platform-page content: page identity (for structured data), one
|
||||
* hero, plus ordered card rows. A route passes exactly this object to
|
||||
* {@link PlatformPage} and nothing else.
|
||||
*/
|
||||
export interface PlatformPageConfig {
|
||||
/** Module name, e.g. "Workflows" - used in the breadcrumb and schema.org name. */
|
||||
module: string
|
||||
/** Canonical path, e.g. "/workflows" - used to build the JSON-LD `url`/breadcrumb. */
|
||||
path: string
|
||||
/** The hero (the page's only `<h1>`). */
|
||||
hero: PlatformHeroConfig
|
||||
/** Card rows rendered in order beneath the logos row. */
|
||||
rows: PlatformCardRowConfig[]
|
||||
}
|
||||
Reference in New Issue
Block a user