Stats bar round 2 (owner): popover centered over the summary button

Round 1 centered the popover over the whole bar ROW — the owner meant
the stats bar's own bottom-left content (the context · cost summary
button). The popover now anchors to a relative wrapper around the
button and centers over IT (left-1/2 -translate-x-1/2 of the wrapper),
so it hangs balanced over the trigger, not offset toward the composer
row's center. Outside-click + Esc closing unchanged from round 1.
This commit is contained in:
amosblomqvist
2026-09-14 10:06:01 +03:00
parent d4b05a0d90
commit e13762fe9a
+17 -13
View File
@@ -14,8 +14,8 @@ import { ModelControl } from "@/components/model-picker";
* All folds come from core/prices.ts (sessionCost, contextWindow, turnCost) * All folds come from core/prices.ts (sessionCost, contextWindow, turnCost)
* — no cost math lives here. Absent data renders "—", never zero (the * — no cost math lives here. Absent data renders "—", never zero (the
* doctrine): no turns yet → every figure is "—"; turns the price table * doctrine): no turns yet → every figure is "—"; turns the price table
* can't price count as unpriced, shown in the popover, never folded into a * can't price count as unpriced, shown in the popover, never folded into
* fake $0. The RIGHT side is the model + thinking control (components/ * a fake $0. The RIGHT side is the model + thinking control (components/
* model-picker.tsx): one controller for the conversation agents, backed by * model-picker.tsx): one controller for the conversation agents, backed by
* pi-ai's catalog; the buttons show the EFFECTIVE model (the record's * pi-ai's catalog; the buttons show the EFFECTIVE model (the record's
* settings, else config's pin), while the left-side figures still fold the * settings, else config's pin), while the left-side figures still fold the
@@ -61,14 +61,18 @@ export function StatsBar({
return ( return (
<div <div
ref={barRef}
data-testid="stats-bar" data-testid="stats-bar"
// pointer-events-auto: the sticky composer wrapper is pointer-events-none // pointer-events-auto: the sticky composer wrapper is pointer-events-none
// (so its transparent gradient area never blocks scrolling) — this row // (so its transparent gradient area never blocks scrolling) — this row
// must re-enable events or every control on it is dead (the model- // must re-enable events or every control on it is dead (the model-
// control wave caught this live: buttons rendered, clicks never landed). // control wave caught this live: buttons rendered, clicks never landed).
ref={barRef}
className="pointer-events-auto relative flex items-center justify-between gap-3 px-3 pt-1.5 text-xs text-muted-foreground" className="pointer-events-auto relative flex items-center justify-between gap-3 px-3 pt-1.5 text-xs text-muted-foreground"
> >
{/* The summary button + its popover: the popover anchors to this
wrapper, CENTERED over the button itself (owner, stats round 2)
— not over the whole bar row, which made it line up oddly. */}
<div className="relative">
<button <button
type="button" type="button"
onClick={() => setOpen((o) => !o)} onClick={() => setOpen((o) => !o)}
@@ -83,16 +87,6 @@ export function StatsBar({
<span aria-hidden>·</span> <span aria-hidden>·</span>
<span data-testid="stats-cost">{costLabel}</span> <span data-testid="stats-cost">{costLabel}</span>
</button> </button>
{/* The model + thinking control — one controller for the conversation
agents (prober/teacher); the record's `turns` above still folds
the ACTUAL models billed, regardless of what the picker says. */}
<ModelControl
chatId={chatId}
settings={settings}
onSaved={onSettingsSaved}
fallbackModelId={lastModel}
/>
{open && ( {open && (
<div <div
data-testid="stats-detail" data-testid="stats-detail"
@@ -145,5 +139,15 @@ export function StatsBar({
</div> </div>
)} )}
</div> </div>
{/* The model + thinking control — one controller for the conversation
agents (prober/teacher); the record's `turns` above still folds
the ACTUAL models billed, regardless of what the picker says. */}
<ModelControl
chatId={chatId}
settings={settings}
onSaved={onSettingsSaved}
fallbackModelId={lastModel}
/>
</div>
); );
} }