Skip to content

Commit 8a8d77f

Browse files
committed
Merge branch 'improvement/platform' of github.com:simstudioai/sim into improvement/platform
2 parents c190d2c + f94052c commit 8a8d77f

12 files changed

Lines changed: 574 additions & 168 deletions

File tree

apps/sim/app/workspace/[workspaceId]/upgrade/components/billing-period-toggle/billing-period-toggle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function BillingPeriodToggle({ isAnnual, onChange, className }: BillingPe
4040
label: (
4141
<>
4242
Annual
43-
<ChipTag variant='blue'>{DISCOUNT_LABEL}</ChipTag>
43+
<ChipTag variant='mono'>{DISCOUNT_LABEL}</ChipTag>
4444
</>
4545
),
4646
},
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/** Cell value for the comparison table. */
2+
export type CellValue = string | boolean
3+
4+
/** Names of the four plan columns — used as a discriminated union for type-safe plan selection. */
5+
export type PlanName = 'Free' | 'Pro' | 'Max' | 'Enterprise'
6+
7+
/** A single feature row inside a section. */
8+
export interface ComparisonRow {
9+
/** Row label displayed in the left column. */
10+
label: string
11+
/**
12+
* Values for [Free, Pro, Max, Enterprise].
13+
* `true` renders a check icon; `false` renders a muted em-dash.
14+
* Strings render as-is with tabular-nums styling.
15+
*/
16+
values: [CellValue, CellValue, CellValue, CellValue]
17+
}
18+
19+
/** A labelled group of comparison rows. */
20+
export interface ComparisonSection {
21+
/** Section header label. */
22+
title: string
23+
rows: ComparisonRow[]
24+
}
25+
26+
/** Column metadata for the four plan headers. */
27+
export interface PlanColumn {
28+
/** Plan name — matches the {@link PlanName} discriminant. */
29+
name: PlanName
30+
/**
31+
* Price display. Pass `null` to signal "use runtime price from state"
32+
* (handled in the component for Pro and Max).
33+
*/
34+
staticPrice: string | null
35+
/** CTA chip label rendered beneath the price. */
36+
ctaLabel: string
37+
/** When true, the CTA chip uses the inverted `primary` (filled) variant to feature this plan. */
38+
highlighted?: boolean
39+
}
40+
41+
/** Ordered plan columns — indices match `ComparisonRow.values`. */
42+
export const PLAN_COLUMNS: PlanColumn[] = [
43+
{ name: 'Free', staticPrice: '$0', ctaLabel: 'Get started' },
44+
{ name: 'Pro', staticPrice: null, ctaLabel: 'Get started', highlighted: true },
45+
{ name: 'Max', staticPrice: null, ctaLabel: 'Get started' },
46+
{ name: 'Enterprise', staticPrice: 'Custom', ctaLabel: 'Talk to sales' },
47+
]
48+
49+
/** Full comparison dataset. */
50+
export const COMPARISON_SECTIONS: ComparisonSection[] = [
51+
{
52+
title: 'Credits & Pricing',
53+
rows: [
54+
{
55+
label: 'Monthly credits',
56+
values: ['1,000 (trial)', '6,000/mo', '25,000/mo', 'Custom'],
57+
},
58+
{
59+
label: 'Daily refresh',
60+
values: [false, '+50/day', '+200/day', 'Custom'],
61+
},
62+
],
63+
},
64+
{
65+
title: 'Rate limits (runs/min)',
66+
rows: [
67+
{
68+
label: 'Sync executions',
69+
values: ['50', '150', '300', 'Custom'],
70+
},
71+
{
72+
label: 'Async executions',
73+
values: ['200', '1,000', '2,500', 'Custom'],
74+
},
75+
{
76+
label: 'API endpoint',
77+
values: ['30', '100', '200', 'Custom'],
78+
},
79+
],
80+
},
81+
{
82+
title: 'Execution timeouts',
83+
rows: [
84+
{
85+
label: 'Sync timeout',
86+
values: ['5 min', '50 min', '50 min', 'Custom'],
87+
},
88+
{
89+
label: 'Async timeout',
90+
values: ['90 min', '90 min', '90 min', 'Custom'],
91+
},
92+
],
93+
},
94+
{
95+
title: 'Storage & data',
96+
rows: [
97+
{
98+
label: 'File storage',
99+
values: ['5 GB', '50 GB', '500 GB', 'Custom'],
100+
},
101+
{
102+
label: 'Max tables',
103+
values: ['5', '100', '1,000', 'Custom'],
104+
},
105+
{
106+
label: 'Max rows per table',
107+
values: ['50,000', '100,000', '500,000', 'Custom'],
108+
},
109+
{
110+
label: 'Log retention',
111+
values: ['30 days', 'Unlimited', 'Unlimited', 'Custom'],
112+
},
113+
],
114+
},
115+
{
116+
title: 'Features',
117+
rows: [
118+
{
119+
label: 'Sim Mailer (Inbox)',
120+
values: [false, false, true, 'Custom'],
121+
},
122+
{
123+
label: 'Live Sync',
124+
values: [false, false, true, 'Custom'],
125+
},
126+
{
127+
label: 'Credential Sets',
128+
values: [false, false, false, 'Custom'],
129+
},
130+
{
131+
label: 'Organizations / Teams',
132+
values: [false, false, false, 'Custom'],
133+
},
134+
{
135+
label: 'Access Control',
136+
values: [false, false, false, 'Custom'],
137+
},
138+
{
139+
label: 'SSO',
140+
values: [false, false, false, 'Custom'],
141+
},
142+
{
143+
label: 'SOC2 Compliance',
144+
values: [false, false, false, 'Custom'],
145+
},
146+
{
147+
label: 'Self Hosting',
148+
values: [false, false, false, 'Custom'],
149+
},
150+
{
151+
label: 'Dedicated Support',
152+
values: [false, false, false, 'Custom'],
153+
},
154+
{
155+
label: 'Seat Management',
156+
values: [false, false, false, 'Custom'],
157+
},
158+
],
159+
},
160+
]
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
'use client'
2+
3+
import { chipVariants } from '@/components/emcn'
4+
import { cn } from '@/lib/core/utils/cn'
5+
import { BillingPeriodToggle } from '@/app/workspace/[workspaceId]/upgrade/components/billing-period-toggle/billing-period-toggle'
6+
import {
7+
type CellValue,
8+
COMPARISON_SECTIONS,
9+
PLAN_COLUMNS,
10+
type PlanName,
11+
} from '@/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data'
12+
13+
/**
14+
* Props for {@link ComparisonTable}.
15+
*/
16+
export interface ComparisonTableProps {
17+
/**
18+
* Resolved Pro price string, e.g. `"$29"`.
19+
* Sourced from the page-level `proPrice` derived from `useUpgradeState`.
20+
*/
21+
proPrice: string
22+
/**
23+
* Resolved Max price string, e.g. `"$79"`.
24+
* Sourced from the page-level `maxPrice` derived from `useUpgradeState`.
25+
*/
26+
maxPrice: string
27+
/**
28+
* Whether annual billing is currently selected.
29+
* Shared with the page-level {@link BillingPeriodToggle} via a single state source.
30+
*/
31+
isAnnual: boolean
32+
/**
33+
* Invoked when the in-table billing toggle changes.
34+
* Should point to the same setter as the page-level toggle.
35+
*/
36+
onIsAnnualChange: (isAnnual: boolean) => void
37+
/** Invoked with the plan name when a column's CTA chip is clicked. */
38+
onSelectPlan: (planName: PlanName) => void
39+
}
40+
41+
/**
42+
* Inline check icon — matches the card-level `CheckIcon` shape.
43+
*/
44+
function CheckIcon() {
45+
return (
46+
<svg
47+
width='14'
48+
height='14'
49+
viewBox='0 0 14 14'
50+
fill='none'
51+
aria-hidden='true'
52+
className='size-[14px] flex-shrink-0'
53+
>
54+
<path
55+
d='M2.5 7L5.5 10L11.5 4'
56+
stroke='currentColor'
57+
strokeWidth='1.5'
58+
strokeLinecap='round'
59+
strokeLinejoin='round'
60+
/>
61+
</svg>
62+
)
63+
}
64+
65+
/**
66+
* Renders a single cell value: `true` → check icon, `false` → em-dash, string → text.
67+
*/
68+
function Cell({ value }: { value: CellValue }) {
69+
if (value === true) {
70+
return (
71+
<span className='flex justify-center text-[var(--text-primary)]'>
72+
<CheckIcon />
73+
</span>
74+
)
75+
}
76+
if (value === false) {
77+
return (
78+
<span className='flex select-none justify-center text-[var(--text-muted)] text-base'></span>
79+
)
80+
}
81+
return (
82+
<span className='block text-center text-[var(--text-primary)] text-small tabular-nums'>
83+
{value}
84+
</span>
85+
)
86+
}
87+
88+
/**
89+
* Full plan-comparison table. Renders all sections from {@link COMPARISON_SECTIONS}
90+
* mapped over generically — no per-cell copy-paste. Prices for Pro and Max are
91+
* supplied as props so they stay in sync with the billing-period toggle in the
92+
* parent page.
93+
*
94+
* The top-left cell contains a "Compare plans" heading, a subtitle, and a
95+
* {@link BillingPeriodToggle} that shares state with the page-level toggle via
96+
* the `isAnnual` / `onIsAnnualChange` props.
97+
*
98+
* @example
99+
* ```tsx
100+
* <ComparisonTable
101+
* proPrice={`$${proPrice}`}
102+
* maxPrice={`$${maxPrice}`}
103+
* isAnnual={state.isAnnual}
104+
* onIsAnnualChange={state.setIsAnnual}
105+
* />
106+
* ```
107+
*/
108+
export function ComparisonTable({
109+
proPrice,
110+
maxPrice,
111+
isAnnual,
112+
onIsAnnualChange,
113+
onSelectPlan,
114+
}: ComparisonTableProps) {
115+
const runtimePrices: Partial<Record<PlanName, string>> = {
116+
Pro: proPrice,
117+
Max: maxPrice,
118+
}
119+
120+
return (
121+
<div className='w-full overflow-x-auto rounded-xl border border-[var(--border-1)]'>
122+
{/* CSS grid: 1 label col + 4 equal plan cols */}
123+
<div className='grid min-w-[640px] grid-cols-[1fr_repeat(4,minmax(0,1fr))]'>
124+
{/* ── Column headers ── */}
125+
{/* Top-left cell: title, subtitle, and billing toggle */}
126+
<div className='flex h-full flex-col justify-between gap-3 border-[var(--border)] border-r bg-[var(--surface-1)] px-4 py-3'>
127+
<div className='flex flex-col gap-0.5'>
128+
<span className='font-medium text-[var(--text-primary)] text-small'>Compare plans</span>
129+
<span className='text-[var(--text-muted)] text-base'>Find the right plan for you</span>
130+
</div>
131+
<BillingPeriodToggle isAnnual={isAnnual} onChange={onIsAnnualChange} />
132+
</div>
133+
134+
{PLAN_COLUMNS.map((col) => {
135+
const price = runtimePrices[col.name] ?? col.staticPrice ?? ''
136+
137+
return (
138+
<div
139+
key={col.name}
140+
className='flex flex-col items-center gap-1 bg-[var(--surface-2)] px-3 py-4 text-center'
141+
>
142+
<span className='font-medium text-[var(--text-primary)] text-base'>{col.name}</span>
143+
<span className='font-medium text-[var(--text-primary)] text-md tabular-nums'>
144+
{price}
145+
</span>
146+
<button
147+
type='button'
148+
onClick={() => onSelectPlan(col.name)}
149+
aria-label={`${col.ctaLabel}${col.name}`}
150+
className={cn(
151+
chipVariants({
152+
variant: col.highlighted ? 'primary' : 'border-shadow',
153+
fullWidth: true,
154+
flush: true,
155+
}),
156+
'mt-2 w-full justify-center'
157+
)}
158+
>
159+
{col.ctaLabel}
160+
</button>
161+
</div>
162+
)
163+
})}
164+
165+
{/* ── Sections ── */}
166+
{COMPARISON_SECTIONS.map((section, sectionIdx) => (
167+
<div key={section.title} className='contents'>
168+
{/* Section header row — split so the left-column separator stays continuous */}
169+
<div
170+
className={cn(
171+
'border-[var(--border)] border-r bg-[var(--surface-1)] px-4 py-2',
172+
sectionIdx > 0 && 'border-[var(--border-1)] border-t'
173+
)}
174+
>
175+
<span className='font-medium text-[var(--text-primary)] text-small'>
176+
{section.title}
177+
</span>
178+
</div>
179+
<div
180+
className={cn(
181+
'col-span-4 bg-[var(--surface-2)]',
182+
sectionIdx > 0 && 'border-[var(--border-1)] border-t'
183+
)}
184+
/>
185+
186+
{/* Feature rows */}
187+
{section.rows.map((row, rowIdx) => (
188+
<div key={row.label} className='contents'>
189+
{/* Label */}
190+
<div
191+
className={cn(
192+
'flex items-center border-[var(--border)] border-r bg-[var(--surface-1)] px-4 py-2.5',
193+
rowIdx < section.rows.length - 1 && 'border-[var(--border-1)] border-b'
194+
)}
195+
>
196+
<span className='text-[var(--text-body)] text-small'>{row.label}</span>
197+
</div>
198+
199+
{/* Plan cells */}
200+
{row.values.map((value, colIdx) => (
201+
<div
202+
key={PLAN_COLUMNS[colIdx].name}
203+
className={cn(
204+
'flex items-center justify-center bg-[var(--surface-2)] px-3 py-2.5',
205+
rowIdx < section.rows.length - 1 && 'border-[var(--border-1)] border-b'
206+
)}
207+
>
208+
<Cell value={value} />
209+
</div>
210+
))}
211+
</div>
212+
))}
213+
</div>
214+
))}
215+
</div>
216+
</div>
217+
)
218+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export type { CellValue, ComparisonRow, ComparisonSection, PlanColumn } from './comparison-data'
2+
export { ComparisonTable, type ComparisonTableProps } from './comparison-table'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export { BillingPeriodToggle, type BillingPeriodToggleProps } from './billing-period-toggle'
22
export { BillingUsageNotificationsToggle } from './billing-usage-notifications-toggle'
3+
export { ComparisonTable, type ComparisonTableProps } from './comparison-table'
4+
export type { PlanName } from './comparison-table/comparison-data'
35
export { CreditBalance, type CreditBalanceProps } from './credit-balance'
46
export { ManagePlanModal, type ManagePlanModalProps } from './manage-plan-modal'
57
export { UpgradePlanCard, type UpgradePlanCardProps } from './plan-card'

0 commit comments

Comments
 (0)