feat(studio): clean up free tier upgrade box (#49851)

## 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?

Small update of the Projects overview UI to resolve the competing CTA's
as well as alignment. Open to tweak a little more here.

| Before | After |
|--------|--------|
| <img width="1287" height="346" alt="Screenshot 2026-09-01 at 15 08 33"
src="https://github.com/user-attachments/assets/a0911d8c-7be9-4a70-ab37-8842093423c9"
/> | <img width="1252" height="430" alt="Screenshot 2026-09-01 at 15 05
40"
src="https://github.com/user-attachments/assets/cd75a8bd-9378-483b-96b8-2272a9990b4c"
/> |


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

* **Style**
* Refined the plan usage card layout with larger metric values and
cleaner labels.
  * Added dividers between usage metrics for improved readability.
* Simplified card and loading-state styling by removing unnecessary
borders, backgrounds, and padding.
  * Updated the upgrade button’s visual treatment.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
kemal.earth authored and GitHub committed 2026-09-02 09:32:36 +01:00
1 parent d088ec6259
commit cecb328120
1 file changed
+20 -48
@@ -14,7 +14,6 @@ type MetricConfig = {
key: PricingMetric
label: string
unit: MetricUnit
/** Anchor id of the matching section on the org usage page. */
anchor: string
}
@@ -40,26 +39,15 @@ const METRICS: MetricConfig[] = [
},
]
const formatGigabytes = (value: number) => {
if (value === 0) return '0 GB'
if (value < 1) return `${(value * 1000).toFixed(0)} MB`
return `${value.toFixed(value < 10 ? 2 : 1)} GB`
}
const formatGigabyteLimit = (limit: number) => {
if (limit < 1) return `${(limit * 1000).toFixed(0)} MB`
return `${limit} GB`
}
// Show counts in full with thousands separators (e.g. `50,000`) rather than abbreviated
// (`50k`), to match the pricing page and avoid ambiguity around plan limits.
const formatCount = (value: number) => value.toLocaleString()
const formatValue = (value: number, unit: MetricUnit) =>
unit === 'gigabytes' ? formatGigabytes(value) : formatCount(value)
const formatLimit = (limit: number, unit: MetricUnit) =>
unit === 'gigabytes' ? formatGigabyteLimit(limit) : formatCount(limit)
const formatUsagePair = (value: number, limit: number, unit: MetricUnit) => {
if (unit === 'count') return { value: formatCount(value), limit: formatCount(limit) }
if (limit < 1) {
return { value: (value * 1000).toFixed(0), limit: `${(limit * 1000).toFixed(0)} MB` }
}
return { value: value === 0 ? '0' : value.toFixed(value < 10 ? 2 : 1), limit: `${limit} GB` }
}
const RING_RADIUS = 7
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS
@@ -113,8 +101,6 @@ const ProgressRing = ({
)
}
// The upgrade CTA surface this card represents. Used as the telemetry `source` +
// `placement` value. Kept as a constant so the tracking stays explicit.
const PLACEMENT = 'org_projects_list'
const CompactMetricRow = ({
@@ -131,26 +117,25 @@ const CompactMetricRow = ({
const ratio = limit > 0 ? current / limit : 0
const isOver = limit > 0 && current >= limit
const isApproaching = limit > 0 && ratio >= 0.8 && !isOver
const formatted = formatUsagePair(current, limit, config.unit)
return (
<Link
href={`/org/${orgSlug}/usage#${config.anchor}`}
className="group/row block px-4 hover:bg-surface-200 transition-colors"
className="group/row block hover:bg-surface-200 transition-colors"
>
<div className="flex items-center justify-between gap-2 py-2 border-t border-dashed">
<div className="flex items-center justify-between gap-2 py-2 border-0">
<div className="flex items-center gap-2 min-w-0">
<ProgressRing ratio={ratio} isOver={isOver} isApproaching={isApproaching} />
<span className="text-[11px] text-foreground-light uppercase font-mono truncate">
{config.label}
</span>
<span className="text-xs text-foreground-light truncate">{config.label}</span>
</div>
<div className="flex items-center shrink-0">
<span className="text-[11px] font-mono tabular-nums whitespace-nowrap">
<span className="text-xs whitespace-nowrap">
<span className={cn(isOver ? 'text-warning' : 'text-foreground')}>
{formatValue(current, config.unit)}
{formatted.value}
</span>
<span className="text-muted"> / </span>
<span className="text-foreground-lighter"> {formatLimit(limit, config.unit)}</span>
<span className="text-foreground-lighter">{formatted.limit}</span>
</span>
<ChevronRight
size={12}
@@ -164,8 +149,8 @@ const CompactMetricRow = ({
}
const SkeletonMetricRow = ({ label }: { label: string }) => (
<div className="px-4">
<div className="flex items-center justify-between gap-2 py-2 border-t border-dashed">
<div>
<div className="flex items-center justify-between gap-2 py-2">
<div className="flex items-center gap-2 min-w-0">
<div
className="w-4 h-4 rounded-full border-2 border-foreground-muted/40 shrink-0"
@@ -178,12 +163,6 @@ const SkeletonMetricRow = ({ label }: { label: string }) => (
</div>
)
/**
* Renders the upgrade CTA's plan-usage card in the org project list (the
* `org_projects_list` CTA placement). The parent is responsible for gating on free plan —
* this component only renders the visual sections once usage data is available. Shaped
* like a `ProjectCard` so it reads as another tile.
*/
export const PlanUsageCard = () => {
const track = useTrack()
const { data: organization } = useSelectedOrganizationQuery()
@@ -199,21 +178,13 @@ export const PlanUsageCard = () => {
}).filter((row): row is { config: MetricConfig; usageItem: OrgMetricsUsage } => row !== null)
: []
// Hide entirely on hard error or when the org has zero applicable metrics — both
// are extreme edge cases. Otherwise always render the card shell so the layout is
// reserved from first paint and the usage rows fade in once the query resolves.
if (isError) return null
if (isSuccess && visibleRows.length === 0) return null
return (
<li className="list-none h-min">
<div
className={cn(
'group relative bg-surface-100 border border-surface rounded-md',
'flex flex-col gap-y-2'
)}
>
<div className="flex items-center justify-between gap-4 p-4 pb-2">
<div className="group relative flex flex-col gap-y-2">
<div className="flex items-start justify-between gap-4 pb-2">
<div className="flex flex-col min-w-0">
<h5 className="text-sm text-foreground truncate">Free plan usage</h5>
<p className="text-xs text-foreground-lighter truncate">Current billing cycle</p>
@@ -222,11 +193,12 @@ export const PlanUsageCard = () => {
<UpgradePlanButton
source={PLACEMENT}
plan="Pro"
variant="default"
onClick={() => track('upgrade_cta_clicked', { placement: PLACEMENT })}
/>
</div>
</div>
<div className="flex flex-col justify-end pb-2 [&>:first-child>*]:border-t-0">
<div className="flex flex-col justify-end pb-2 [&>:first-child>*]:border-t-0 divide-y">
{isSuccess
? visibleRows.map(({ config, usageItem }) => (
<CompactMetricRow