fix(studio): alignment of compute pricing blocks on compute settings (#48289)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Fixes alignment issues across viewports with the size blocks on compute
& disk page.





<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Added responsive container support for horizontal form layouts.
* Compute-size options now adapt more smoothly to available screen
space.
* Preserved existing loading states, error messages, pricing details,
locked options, and larger-compute guidance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
kemal.earth authored and GitHub committed 2026-07-24 17:35:50 +01:00
1 parent 8d4d3b57e0
commit 84e273cf0b
2 files changed
+206 -185

No files matched your search

@@ -139,6 +139,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
>
<FormItemLayout
layout="horizontal"
isContainerResponsive
label="Compute size"
id={field.name}
className="gap-5"
@@ -178,201 +179,197 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
</>
}
>
<div
className={
!addonsError
? 'grid gap-4 grid-cols-[repeat(auto-fit,minmax(min(100%,13em),1fr))]'
: ''
}
>
{isLoading ? (
Array(INITIALLY_VISIBLE_COUNT)
.fill(0)
.map((_, i) => <Skeleton key={i} className="w-full h-[110px] rounded-md" />)
) : addonsError ? (
<FormMessage message={'Failed to load Compute size options'} type="error">
<p>{addonsError?.message}</p>
</FormMessage>
) : (
<>
{visibleOptions.map((compute) => {
const lockedMicroDueToPITR =
compute.identifier === 'ci_micro' && !!subscriptionPitr
const lockedNanoDueToPlan =
org?.plan.id !== 'free' &&
project?.infra_compute_size !== 'nano' &&
compute.identifier === 'ci_nano'
<div className="@container">
<div className={!addonsError ? 'grid gap-4 grid-cols-2 @[680px]:grid-cols-3' : ''}>
{isLoading ? (
Array(INITIALLY_VISIBLE_COUNT)
.fill(0)
.map((_, i) => <Skeleton key={i} className="w-full h-[110px] rounded-md" />)
) : addonsError ? (
<FormMessage message={'Failed to load Compute size options'} type="error">
<p>{addonsError?.message}</p>
</FormMessage>
) : (
<>
{visibleOptions.map((compute) => {
const lockedMicroDueToPITR =
compute.identifier === 'ci_micro' && !!subscriptionPitr
const lockedNanoDueToPlan =
org?.plan.id !== 'free' &&
project?.infra_compute_size !== 'nano' &&
compute.identifier === 'ci_nano'
const lockedOption = lockedNanoDueToPlan || lockedMicroDueToPITR
const lockedOption = lockedNanoDueToPlan || lockedMicroDueToPITR
const price =
org?.plan.id !== 'free' &&
project?.infra_compute_size === 'nano' &&
compute.identifier === 'ci_nano'
? availableOptions.find(
(option: ComputeAddonVariant) => option.identifier === 'ci_micro'
)?.price
: compute.price
const price =
org?.plan.id !== 'free' &&
project?.infra_compute_size === 'nano' &&
compute.identifier === 'ci_nano'
? availableOptions.find(
(option: ComputeAddonVariant) => option.identifier === 'ci_micro'
)?.price
: compute.price
const cpuLabel = (() => {
const cpuCores = compute.meta?.cpu_cores
if (typeof cpuCores === 'number') {
return `${cpuCores}-core CPU`
}
if (cpuCores) {
return `${cpuCores} CPU`
}
return 'CPU'
})()
const cpuLabel = (() => {
const cpuCores = compute.meta?.cpu_cores
if (typeof cpuCores === 'number') {
return `${cpuCores}-core CPU`
}
if (cpuCores) {
return `${cpuCores} CPU`
}
return 'CPU'
})()
return (
<RadioGroupCardItem
showIndicator={false}
id={compute.identifier}
key={compute.identifier}
value={compute.identifier}
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]',
lockedOption && 'opacity-50'
)}
disabled={disabled || lockedOption}
label={
<Tooltip>
<TooltipTrigger asChild>
<div>
{showUpgradeBadge && compute.identifier === 'ci_micro' && (
<div className="absolute -top-4 -right-3 text-violet-1100 flex items-center gap-1 bg-surface-75 py-0.5 px-2 rounded-full border border-violet-900">
<span>No additional charge</span>
</div>
)}
<div className="w-full flex flex-col gap-3 justify-between">
<div className="relative px-3 opacity-50 group-data-checked:opacity-100 flex justify-between">
<ComputeBadge
className="inline-flex font-semibold"
infraComputeSize={compute.name as InfraInstanceSize}
/>
<div className="flex items-center space-x-1">
{lockedOption ? (
<div className="bg border rounded-lg h-7 w-7 flex items-center justify-center">
<Lock size={14} />
</div>
) : (
showComputePrice && (
<>
<span
className="text-foreground text-sm font-semibold"
translate="no"
>
${price}
</span>
<span className="text-foreground-light translate-y-px">
{' '}
/{' '}
{compute.price_interval === 'monthly'
? 'month'
: 'hour'}
</span>
</>
)
)}
return (
<RadioGroupCardItem
showIndicator={false}
id={compute.identifier}
key={compute.identifier}
value={compute.identifier}
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]',
lockedOption && 'opacity-50'
)}
disabled={disabled || lockedOption}
label={
<Tooltip>
<TooltipTrigger asChild>
<div>
{showUpgradeBadge && compute.identifier === 'ci_micro' && (
<div className="absolute -top-4 -right-3 text-violet-1100 flex items-center gap-1 bg-surface-75 py-0.5 px-2 rounded-full border border-violet-900">
<span>No additional charge</span>
</div>
</div>
<div className="w-full">
<div className="px-3 text-sm flex flex-col gap-1">
<div className="text-foreground-light flex gap-2 items-center">
<Microchip
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>
{compute.identifier === 'ci_nano' && 'Up to '}
{compute.meta?.memory_gb ?? 0} GB memory
</span>
)}
<div className="w-full flex flex-col gap-3 justify-between">
<div className="relative px-3 opacity-50 group-data-checked:opacity-100 flex justify-between">
<ComputeBadge
className="inline-flex font-semibold"
infraComputeSize={compute.name as InfraInstanceSize}
/>
<div className="flex items-center space-x-1">
{lockedOption ? (
<div className="bg border rounded-lg h-7 w-7 flex items-center justify-center">
<Lock size={14} />
</div>
) : (
showComputePrice && (
<>
<span
className="text-foreground text-sm font-semibold"
translate="no"
>
${price}
</span>
<span className="text-foreground-light translate-y-px">
{' '}
/{' '}
{compute.price_interval === 'monthly'
? 'month'
: 'hour'}
</span>
</>
)
)}
</div>
<div className="text-foreground-light flex gap-2 items-center">
<CpuIcon
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>{cpuLabel}</span>
</div>
<div className="w-full">
<div className="px-3 text-sm flex flex-col gap-1">
<div className="text-foreground-light flex gap-2 items-center">
<Microchip
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>
{compute.identifier === 'ci_nano' && 'Up to '}
{compute.meta?.memory_gb ?? 0} GB memory
</span>
</div>
<div className="text-foreground-light flex gap-2 items-center">
<CpuIcon
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>{cpuLabel}</span>
</div>
</div>
</div>
</div>
</div>
</TooltipTrigger>
{lockedMicroDueToPITR && (
<TooltipContent side="bottom" className="w-64 text-center">
Project has PITR enabled which requires a minimum of Small
compute. Please{' '}
<InlineLink href="/project/_/settings/addons?panel=pitr">
disable PITR
</InlineLink>{' '}
first before selecting Micro
</TooltipContent>
)}
</Tooltip>
}
/>
)
})}
{showAllSizes && (
<RadioGroupCardItem
id="larger-compute"
key="larger-compute"
showIndicator={false}
value="larger-compute"
onClick={(e) => e.preventDefault()}
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]'
)}
label={
<SupportLink
queryParams={{
projectRef: ref,
category: SupportCategories.SALES_ENQUIRY,
subject: 'Enquiry about larger instance sizes',
}}
>
<div className="w-full flex flex-col gap-3 justify-between">
<div className="relative px-3 flex justify-between">
<ComputeBadge infraComputeSize=">16XL" />
<div className="flex items-center space-x-1 opacity-50 ">
<span className="text-foreground-light text-sm">Contact Us</span>
</div>
</div>
</TooltipTrigger>
{lockedMicroDueToPITR && (
<TooltipContent side="bottom" className="w-64 text-center">
Project has PITR enabled which requires a minimum of Small compute.
Please{' '}
<InlineLink href="/project/_/settings/addons?panel=pitr">
disable PITR
</InlineLink>{' '}
first before selecting Micro
</TooltipContent>
)}
</Tooltip>
<div className="w-full">
<div className="px-3 text-sm flex flex-col gap-1">
<div className="text-foreground-light flex gap-2 items-center">
<Microchip
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>Custom memory</span>
</div>
<div className="text-foreground-light flex gap-2 items-center">
<CpuIcon
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>Custom CPU</span>
</div>
</div>
</div>
</div>
</SupportLink>
}
/>
)
})}
{showAllSizes && (
<RadioGroupCardItem
id="larger-compute"
key="larger-compute"
showIndicator={false}
value="larger-compute"
onClick={(e) => e.preventDefault()}
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]'
)}
label={
<SupportLink
queryParams={{
projectRef: ref,
category: SupportCategories.SALES_ENQUIRY,
subject: 'Enquiry about larger instance sizes',
}}
>
<div className="w-full flex flex-col gap-3 justify-between">
<div className="relative px-3 flex justify-between">
<ComputeBadge infraComputeSize=">16XL" />
<div className="flex items-center space-x-1 opacity-50 ">
<span className="text-foreground-light text-sm">Contact Us</span>
</div>
</div>
<div className="w-full">
<div className="px-3 text-sm flex flex-col gap-1">
<div className="text-foreground-light flex gap-2 items-center">
<Microchip
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>Custom memory</span>
</div>
<div className="text-foreground-light flex gap-2 items-center">
<CpuIcon
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>Custom CPU</span>
</div>
</div>
</div>
</div>
</SupportLink>
}
/>
)}
</>
)}
)}
</>
)}
</div>
</div>
{!isLoading && !addonsError && hasHiddenOptions && (
@@ -18,6 +18,14 @@ type Props = {
isReactForm?: boolean
hideMessage?: boolean
name?: string
/**
* For layout="horizontal", switches the label/content split to react to the nearest
* `@container` ancestor's width instead of the viewport width. Use when the layout sits
* inside a narrower column (e.g. next to a sidebar) so it stacks to a single column as
* soon as that column gets tight, rather than waiting for a viewport breakpoint that may
* never reflect the column's actual available space.
*/
isContainerResponsive?: boolean
}
const ContainerVariants = cva('relative grid gap-10', {
@@ -35,7 +43,7 @@ const ContainerVariants = cva('relative grid gap-10', {
false: '',
},
layout: {
horizontal: 'flex flex-col gap-2 md:grid md:grid-cols-12',
horizontal: 'flex flex-col gap-2',
vertical: 'flex flex-col gap-2',
flex: 'flex flex-row gap-3',
'flex-row-reverse':
@@ -57,8 +65,20 @@ const ContainerVariants = cva('relative grid gap-10', {
align: 'right',
className: 'justify-between',
},
{
layout: 'horizontal',
responsive: false,
className: 'md:grid md:grid-cols-12',
},
{
layout: 'horizontal',
responsive: true,
className: '@xl:grid @xl:grid-cols-12',
},
],
defaultVariants: {},
defaultVariants: {
responsive: false,
},
})
const LabelContainerVariants = cva('transition-all duration-500 ease-in-out', {
@@ -287,6 +307,7 @@ export const FormLayout = React.forwardRef<
hideMessage = false,
isReactForm,
error,
isContainerResponsive = false,
...props
},
ref
@@ -356,7 +377,10 @@ export const FormLayout = React.forwardRef<
<div
ref={ref}
{...props}
className={cn(ContainerVariants({ size, flex, align, layout }), className)}
className={cn(
ContainerVariants({ size, flex, align, layout, responsive: isContainerResponsive }),
className
)}
>
{flex && (
<div className={cn(FlexContainer({ flex, align, layout }))}>