diff --git a/frontend/src/app/billing/billing-limit-alert.tsx b/frontend/src/app/billing/billing-limit-alert.tsx index 0a0aaaae90..55cd3fde67 100644 --- a/frontend/src/app/billing/billing-limit-alert.tsx +++ b/frontend/src/app/billing/billing-limit-alert.tsx @@ -2,7 +2,7 @@ import { faExclamationTriangle, Icon } from "@rivet-gg/icons"; import { useQuery } from "@tanstack/react-query"; import { Link, useMatch } from "@tanstack/react-router"; import { AnimatePresence, motion } from "framer-motion"; -import { useEffect } from "react"; +import { useLayoutEffect } from "react"; import { Button, cn } from "@/components"; import { useCloudProjectDataProvider } from "@/components/actors"; import { PLAN_LABELS } from "@/content/billing"; @@ -44,7 +44,9 @@ function BillingLimitAlertInner() { const plan = billingData?.billing.activePlan || "free"; const hidden = plan !== "free" || usagePercent < 80; - useEffect(() => { + // Layout effect so the drawer's `top` moves in the same paint the banner + // mounts in, instead of a frame where the drawer still sits behind it. + useLayoutEffect(() => { if (hidden) return; const root = document.documentElement; root.style.setProperty("--billing-banner-height", BANNER_HEIGHT); @@ -66,7 +68,10 @@ function BillingLimitAlertInner() { exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.25, ease: "easeOut" }} className={cn( - "overflow-hidden border-b", + // Above the settings drawer (`z-40`) so it stays visible + // while the drawer's `top` catches up; below `z-50` + // popovers, dropdowns, and dialogs. + "relative z-[45] overflow-hidden border-b", atLimit ? "border-destructive/60 bg-destructive/15" : "border-warning/60 bg-warning/10", diff --git a/frontend/src/app/settings-drawer.tsx b/frontend/src/app/settings-drawer.tsx index 380dee2d8a..28a3f2adfa 100644 --- a/frontend/src/app/settings-drawer.tsx +++ b/frontend/src/app/settings-drawer.tsx @@ -185,11 +185,15 @@ export function SettingsDrawer({ = 100; + const exhaustedNames = [ + ...USAGE_METRICS.filter( + (metric) => (metricsByKey.get(metric.key)?.percent ?? 0) >= 100, + ).map((metric) => metric.title), + ...(showCompute && usage.computeBudgetPercent >= 100 + ? ["Compute"] + : []), + ]; + return (
+ {planExhausted ? ( + setPlansOpen(true)} + /> + ) : null} +
void; +}) { + const what = + exhaustedNames.length > 0 + ? `${formatList(exhaustedNames)} ${exhaustedNames.length === 1 ? "has" : "have"} reached the Free plan's included limit for this billing period.` + : "This project has reached the Free plan's included limits for this billing period."; + return ( + +
+
+ +
+
+

+ You've used your entire Free plan +

+

+ {what} Upgrade to raise your limits and avoid service + interruptions. +

+
+ +
+
+ ); +} + +function formatList(items: string[]): string { + if (items.length <= 1) return items[0] ?? ""; + if (items.length === 2) return `${items[0]} and ${items[1]}`; + return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`; +} + function CurrentPlanCard({ plan, onUpgrade, @@ -433,11 +504,17 @@ function ComputeUsageRow({
- {loading ? : formatCurrency(cost)} + {loading ? ( + + ) : ( + formatCurrency(cost) + )}
- {capUsd != null ? `of ${formatCurrency(capUsd)}` : "No limit"} + {capUsd != null + ? `of ${formatCurrency(capUsd)}` + : "No limit"}
{capUsd != null ? (