Skip to content
56 changes: 56 additions & 0 deletions apps/web/src/components/usage/UsageLimitsPooled.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,59 @@ it("omits an empty reset plate but keeps countdowns and banked credits visible",
await expect.element(plate()).toBeVisible();
expect(plate()?.textContent).toBe("1");
});

it("moves the one limit popover between windows on hover", async () => {
const [environmentId, presentation] = [...presentations("2026-09-03T14:00:00Z", 1)][0]!;
const serverConfig = presentation.serverConfig!;
const provider = serverConfig.providers[0]!;
const twoWindows: LimitPresentations = new Map([
[
environmentId,
{
...presentation,
serverConfig: {
...serverConfig,
providers: [
{
...provider,
usageLimits: {
...provider.usageLimits!,
windows: [
...provider.usageLimits!.windows,
{ id: "weekly", kind: "weekly", label: "Weekly", usedPercent: 32 },
],
},
},
],
},
},
],
]);
// Room above the bars so the popover opens there instead of flipping over the next card.
container.style.paddingTop = "20rem";
await act(() => root.render(<UsageLimitsPooled presentations={twoWindows} now={0} />));
const segment = (label: string) =>
Array.from(document.querySelectorAll("button")).find((node) =>
node.getAttribute("aria-label")?.startsWith(label),
)!;
// Popup contents, once the previous segment's details have animated away.
const popups = () =>
document.querySelector("[data-previous]")
? null
: Array.from(
document.querySelectorAll('[data-slot="popover-popup"]'),
(node) => node.textContent,
);
const session = segment("Alpha: 60%");
const weekly = segment("Alpha: 68%");

await act(() => userEvent.click(session));
await expect.poll(popups).toEqual([expect.stringContaining("Left60%")]);

await act(() => userEvent.hover(weekly));
await expect.poll(popups).toEqual([expect.stringContaining("Left68%")]);

// Weekly's popover now sits over the middle of the Session bar; come back in at its edge.
await act(() => userEvent.hover(session, { position: { x: 4, y: 4 } }));
await expect.poll(popups).toEqual([expect.stringContaining("Left60%")]);
});
188 changes: 99 additions & 89 deletions apps/web/src/components/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
remainingPercent,
} from "@t3tools/shared/usageLimits";
import { AlertTriangleIcon, ExternalLinkIcon, TicketIcon } from "lucide-react";
import { Fragment, type ReactNode, useState } from "react";
import { createContext, Fragment, type ReactNode, use, useState } from "react";

import { ensureLocalApi } from "../../localApi";
import { usePrimarySettings } from "../../hooks/useSettings";
Expand All @@ -26,7 +26,7 @@ import { RedactedSensitiveText } from "../settings/RedactedSensitiveText";
import { Button } from "../ui/button";
import { OpenAI } from "../Icons";
import { Alert, AlertTitle } from "../ui/alert";
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
import { Popover, PopoverCreateHandle, PopoverPopup, PopoverTrigger } from "../ui/popover";
import {
PaceIcon,
ResetCreditDialog,
Expand Down Expand Up @@ -201,36 +201,94 @@ function SegmentPopover({
);
}

/**
* Every segment on the page shares one popover, so hovering or clicking another
* segment moves it instead of opening a second one. The payload is the segment
* it currently describes.
*/
type SegmentPayload = Omit<Parameters<typeof SegmentPopover>[0], "now" | "onRedeem">;
const SegmentPopoverContext = createContext<ReturnType<
typeof PopoverCreateHandle<SegmentPayload>
> | null>(null);

/**
* The confirm is a sibling of the popover, not a child: dialogs stack under
* popovers, and the popover closes as the confirm opens.
*/
function PoolSegment({
account,
...props
}: {
readonly account: LimitAccount;
readonly window: LimitPoolMember["window"];
readonly reset: LimitPoolWindow["resets"][number] | undefined;
readonly refillPercent: number;
readonly color: string;
readonly now: number;
readonly index: number;
readonly showAccountName: boolean;
}) {
return account.redeem ? (
<RedeemableSegment account={account} redeemAt={account.redeem} {...props} />
) : (
<SegmentTriggers account={account} redeem={null} {...props} />
);
}

function RedeemableSegment({
redeemAt,
...props
}: Omit<Parameters<typeof SegmentTriggers>[0], "redeem"> & {
readonly redeemAt: NonNullable<LimitAccount["redeem"]>;
}) {
const redeem = useResetCredit(redeemAt.environmentId, redeemAt.input);
return (
<>
<SegmentTriggers redeem={redeem} {...props} />
<ResetCreditDialog
open={redeem.confirming}
onOpenChange={redeem.setConfirming}
onConfirm={() => void redeem.redeem()}
disabled={!redeem.canManageProviders}
/>
{/* The popover closed before the confirm, so progress and the outcome need a home outside it. */}
{redeem.busy || redeem.status ? (
<span role="status" className="col-span-full text-xs text-muted-foreground">
<AccountName account={props.account} className="font-medium text-foreground" />{" "}
{redeem.busy ? "Using reset credit…" : redeem.status}
</span>
) : null}
</>
);
}

function SegmentTriggers({
account,
window,
reset,
redeem,
refillPercent,
color,
now,
index,
showAccountName,
}: {
readonly account: LimitAccount;
readonly window: LimitPoolMember["window"];
readonly reset: LimitPoolWindow["resets"][number] | undefined;
}: SegmentPayload & {
readonly refillPercent: number;
readonly color: string;
readonly now: number;
readonly index: number;
readonly showAccountName: boolean;
}) {
const [open, setOpen] = useState(false);
const popover = use(SegmentPopoverContext) ?? undefined;
const payload: SegmentPayload = { account, window, reset, redeem };
const remaining = remainingPercent(window);
const resetsIn = formatResetsIn(window, now);
const credits = account.limits.resetCredits?.availableCount ?? 0;
return (
<Popover open={open} onOpenChange={setOpen}>
<>
<PopoverTrigger
handle={popover}
payload={payload}
openOnHover
render={
<button
Expand Down Expand Up @@ -290,50 +348,31 @@ function PoolSegment({
) : null}
</div>
</PopoverTrigger>
<LegendRow account={account} window={window} color={color} now={now} index={index} />
{account.redeem ? (
<RedeemableSegmentPopup
account={account}
window={window}
reset={reset}
now={now}
redeemAt={account.redeem}
closePopover={() => setOpen(false)}
/>
) : (
<PopoverPopup side="top" sideOffset={6}>
<SegmentPopover
account={account}
window={window}
reset={reset}
now={now}
redeem={null}
onRedeem={() => {}}
/>
</PopoverPopup>
)}
</Popover>
<LegendRow payload={payload} color={color} now={now} index={index} />
</>
);
}

function LegendRow({
account,
window,
payload,
color,
now,
index,
}: {
readonly account: LimitAccount;
readonly window: LimitPoolMember["window"];
readonly payload: SegmentPayload;
readonly color: string;
readonly now: number;
readonly index: number;
}) {
const popover = use(SegmentPopoverContext) ?? undefined;
const { account, window } = payload;
const remaining = remainingPercent(window);
const resetsIn = formatResetsIn(window, now);
const credits = account.limits.resetCredits?.availableCount ?? 0;
return (
<PopoverTrigger
handle={popover}
payload={payload}
style={{ gridColumn: "1 / -1", gridRow: index + 1 }}
render={<Button variant="ghost" size="compact" />}
className="min-w-0 @2xl/pool:hidden"
Expand Down Expand Up @@ -371,54 +410,6 @@ function LegendRow({
);
}

function RedeemableSegmentPopup({
account,
window,
reset,
now,
redeemAt,
closePopover,
}: {
readonly account: LimitAccount;
readonly window: LimitPoolMember["window"];
readonly reset: LimitPoolWindow["resets"][number] | undefined;
readonly now: number;
readonly redeemAt: NonNullable<LimitAccount["redeem"]>;
readonly closePopover: () => void;
}) {
const redeem = useResetCredit(redeemAt.environmentId, redeemAt.input);
return (
<>
<PopoverPopup side="top" sideOffset={6}>
<SegmentPopover
account={account}
window={window}
reset={reset}
now={now}
redeem={redeem}
onRedeem={() => {
closePopover();
redeem.setConfirming(true);
}}
/>
</PopoverPopup>
<ResetCreditDialog
open={redeem.confirming}
onOpenChange={redeem.setConfirming}
onConfirm={() => void redeem.redeem()}
disabled={!redeem.canManageProviders}
/>
{/* The popover closed before the confirm, so progress and the outcome need a home outside it. */}
{redeem.busy || redeem.status ? (
<span role="status" className="col-span-full text-xs text-muted-foreground">
<AccountName account={account} className="font-medium text-foreground" />{" "}
{redeem.busy ? "Using reset credit…" : redeem.status}
</span>
) : null}
</>
);
}

function PoolBar({
pool,
color,
Expand Down Expand Up @@ -538,6 +529,7 @@ export function UsageLimitsPooled({
readonly now: number;
readonly cursorPrompt?: ReactNode;
}) {
const [segmentPopover] = useState(() => PopoverCreateHandle<SegmentPayload>());
const pools = collectLimitPools(collectLimitAccounts(presentations), now);
const notices = collectLimitNotices(presentations);
const externalLinks = collectExternalUsageLinks(presentations);
Expand All @@ -553,12 +545,30 @@ export function UsageLimitsPooled({
No provider on the selected environments reports subscription limits.
</p>
) : null}
{pools.map((pool, index) => (
<Fragment key={pool.driver}>
{index === cursorPromptAt ? cursorPrompt : null}
<PoolSection pool={pool} now={now} />
</Fragment>
))}
<SegmentPopoverContext value={segmentPopover}>
{pools.map((pool, index) => (
<Fragment key={pool.driver}>
{index === cursorPromptAt ? cursorPrompt : null}
<PoolSection pool={pool} now={now} />
</Fragment>
))}
</SegmentPopoverContext>
<Popover handle={segmentPopover}>
{({ payload }) =>
payload ? (
<PopoverPopup side="top" sideOffset={6}>
<SegmentPopover
{...payload}
now={now}
onRedeem={() => {
segmentPopover.close();
payload.redeem?.setConfirming(true);
}}
/>
</PopoverPopup>
) : null
}
</Popover>
{cursorPromptAt === pools.length ? cursorPrompt : null}
{externalLinks.map((link) => (
<section
Expand Down
Loading