Files
wehub-resource-sync d25d482dc2
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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

139 lines
4.7 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Check, ChipLink, ChipTag, cn } from '@sim/emcn'
import { SlackIcon } from '@/components/icons'
import type { CellValue } from '@/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data'
/** Maps a cell-icon identifier to its brand icon component. */
const CELL_ICONS = { slack: SlackIcon } as const
/** Resolved CTA for a pricing card - label, chip variant, and destination href. */
export interface PricingCardCta {
label: string
variant: 'primary' | 'border-shadow'
href: string
}
/** A labelled group of feature rows for one plan, transposed from the comparison data. */
export interface PricingCardSection {
/** Stable key for the section, used for React reconciliation. */
key: string
/** Section header, e.g. `"Rate limits (runs/min)"`. Omit to render the rows without a header. */
title?: string
/** Feature rows - this plan's value for each comparison row in the section. */
rows: { label: string; value: CellValue }[]
}
/**
* Props for {@link PricingCard}.
*/
export interface PricingCardProps {
/** Plan name, e.g. `"Pro"`. */
name: string
/** Headline price, e.g. `"$25"`, `"$0"`, or `"Custom"`. */
price: string
/** Small line under the price, e.g. `"per user/month, billed monthly"`. */
priceSubtext?: string
/** Optional discount pill next to the price, e.g. `"15% off"`. */
discountLabel?: string
/** Resolved CTA - label, variant, and destination href. */
cta: PricingCardCta
/** Full feature breakdown for this plan, grouped by section. */
sections: PricingCardSection[]
/** Extra outer classes. */
className?: string
}
/**
* Renders one plan's value for a feature row: `true` → check, `false` → em-dash,
* an icon reference → its brand icon, a string → right-aligned text.
*/
function FeatureValue({ value }: { value: CellValue }) {
if (value === true) {
return <Check className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
}
if (value === false) {
return <span className='select-none text-[var(--text-muted)]'></span>
}
if (typeof value === 'object') {
const Icon = CELL_ICONS[value.icon]
return <Icon className='size-[14px] flex-shrink-0' />
}
return (
<span className='whitespace-nowrap text-right text-[var(--text-primary)] text-sm tabular-nums'>
{value}
</span>
)
}
/**
* Public pricing card - one plan rendered as a self-contained spec sheet: the
* header (name, price, CTA, segment) above the full comparison breakdown, grouped
* by section with a hairline rule under each section header.
*
* The radius (`rounded-lg`) matches the landing hero's visual panel so the cards
* line up with the rest of the landing chrome. Every card carries the identical
* row structure (only values differ) so a grid of them renders at exactly the
* same height. Feature rows are transposed from the shared comparison data, so
* the card can never drift from the platform.
*/
export function PricingCard({
name,
price,
priceSubtext,
discountLabel,
cta,
sections,
className,
}: PricingCardProps) {
return (
<article
className={cn(
'flex h-full flex-col gap-[22px] rounded-lg border border-[var(--border-1)] bg-[var(--surface-2)] p-5',
className
)}
>
<div className='flex flex-col gap-[22px]'>
<h2 className='text-[24px] text-[var(--text-primary)]'>{name}</h2>
<div className='flex flex-col'>
<div className='flex items-center gap-2'>
<span className='text-[20px] text-[var(--text-primary)] tabular-nums'>{price}</span>
{discountLabel && <ChipTag variant='mono'>{discountLabel}</ChipTag>}
</div>
<p className='text-[var(--text-muted)] text-base'>{priceSubtext ?? ' '}</p>
</div>
<ChipLink
href={cta.href}
variant={cta.variant}
fullWidth
flush
className='w-full justify-center'
>
{cta.label}
</ChipLink>
</div>
<div className='flex flex-col gap-5'>
{sections.map((section) => (
<div key={section.key} className='flex flex-col'>
{section.title && (
<>
<span className='text-[var(--text-primary)] text-small'>{section.title}</span>
<div className='mt-2 mb-2.5 h-px bg-[var(--border)]' />
</>
)}
<div className='flex flex-col gap-2.5'>
{section.rows.map((row) => (
<div key={row.label} className='flex items-center justify-between gap-3'>
<span className='text-[var(--text-body)] text-sm'>{row.label}</span>
<FeatureValue value={row.value} />
</div>
))}
</div>
</div>
))}
</div>
</article>
)
}