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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -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'
@@ -0,0 +1,2 @@
export { PlatformCard } from './platform-card'
export { PlatformPillCta } from './platform-pill-cta'
@@ -0,0 +1 @@
export { PlatformCard } from './platform-card'
@@ -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>
)
}
@@ -0,0 +1 @@
export { PlatformPillCta } from './platform-pill-cta'
@@ -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'
@@ -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'
@@ -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'
@@ -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>
)
}
@@ -0,0 +1 @@
export { PlatformStructuredData } from './platform-structured-data'
@@ -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} />
}
@@ -0,0 +1 @@
export { PlatformVisualFrame } from './platform-visual-frame'
@@ -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[]
}