Pickers: anchor the desktop dropdown to its chip, never clipped
"The model selection in teach it back is kinda broken at least on desktop": the desktop panels were position:absolute children of whatever container held their chip. In chat that is the composer at the bottom of the column, which works, but Teach it back keeps its chips inside a scrolling form card — so the panel opened upward out of the card and its top rows (including the selected model) were cut off. The probe measured top: -66px and no way to reach the first rows by scrolling the list. - New components/chat/PickerPopover.tsx: the desktop surface for both pickers, portalled to <body> and position:fixed, anchored to the opening chip ([data-picker-trigger][aria-expanded="true"]), opening on the side with room and capped to the viewport so the panel scrolls as a whole with its heading and note attached. It closes on an outside press, Escape, resize or a page scroll — but not on scrolling its own list. - ContextPicker and ModelPicker use it on desktop; the phone sheets and their inner list caps are unchanged (the list max-height became a parameter: the sheet caps it, the desktop panel scrolls instead). - teach-test pins it: the panel's rect is fully inside the viewport, elementFromPoint over it lands on the panel, it is not a descendant of the scrolling card, and picking a model updates the chip + freezmo.chat.model.v1 (46 checks). Chat and the phone sheets re-verified unchanged (110 + 128). Version 0.8.3, SW v105.
This commit is contained in:
parent
d72b335965
commit
705808d45a
7 changed files with 215 additions and 82 deletions
|
|
@ -1,9 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
|
||||
|
||||
import { PickerPopover } from "./PickerPopover";
|
||||
import { CheckCircleIcon } from "@/components/icons";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { providerInfo } from "@/lib/providers";
|
||||
|
|
@ -36,7 +37,6 @@ export function ModelPicker({
|
|||
const { t } = useI18n();
|
||||
const isPhone = useIsPhone();
|
||||
const reduceMotion = useReducedMotion();
|
||||
const panel = useRef<HTMLDivElement>(null);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
/* Every model the server lists is offered, so the list can be long — filter
|
||||
|
|
@ -56,27 +56,14 @@ export function ModelPicker({
|
|||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [open, onClose]);
|
||||
|
||||
/* Desktop dropdown: a press anywhere outside closes it, except on the
|
||||
trigger chips, which toggle their own popover (see Composer). */
|
||||
useEffect(() => {
|
||||
if (!open || isPhone) return;
|
||||
const onDown = (event: PointerEvent) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
if (panel.current?.contains(target)) return;
|
||||
if (target.closest("[data-picker-trigger]")) return;
|
||||
onClose();
|
||||
};
|
||||
document.addEventListener("pointerdown", onDown);
|
||||
return () => document.removeEventListener("pointerdown", onDown);
|
||||
}, [open, isPhone, onClose]);
|
||||
|
||||
const providerLabel = (id: string) => providerInfo(id)?.label ?? id;
|
||||
|
||||
const badge = (model: ChatModelChoice) =>
|
||||
model.badge === "vision" ? t("chat.modelVision") : model.badge === "both" ? t("chat.modelBoth") : t("chat.modelText");
|
||||
|
||||
const rows = (
|
||||
/** `listMax` is a CSS max-height for the list: the phone sheet caps it, the
|
||||
* desktop panel scrolls as a whole instead (see PickerPopover). */
|
||||
const rows = (listMax: string) => (
|
||||
<>
|
||||
{models.length > 8 ? (
|
||||
<input
|
||||
|
|
@ -89,7 +76,7 @@ export function ModelPicker({
|
|||
className="mb-2 w-full rounded-[var(--radius-control)] border border-[var(--glass-border)] bg-[var(--surface-sunken)] px-3 py-2 text-[13px] text-[var(--text)] outline-none placeholder:text-[var(--text-subtle)] focus:border-accent-400"
|
||||
/>
|
||||
) : null}
|
||||
<ul className="max-h-[60dvh] space-y-1 overflow-y-auto overscroll-contain">
|
||||
<ul className="space-y-1 overflow-y-auto overscroll-contain" style={{ maxHeight: listMax }}>
|
||||
{visible.length === 0 ? (
|
||||
<li className="px-3 py-2 text-[12.5px] text-[var(--text-subtle)]">{t("chat.modelSearchEmpty")}</li>
|
||||
) : null}
|
||||
|
|
@ -160,7 +147,7 @@ export function ModelPicker({
|
|||
>
|
||||
<div className="sheet-grabber mx-auto" aria-hidden />
|
||||
<h2 className="px-1 pb-3 pt-2 text-[15px] font-semibold text-[var(--text)]">{t("chat.modelTitle")}</h2>
|
||||
{rows}
|
||||
{rows("60dvh")}
|
||||
<p className="px-1 py-3 text-[11.5px] text-[var(--text-subtle)]">{t("chat.modelNote")}</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
|
@ -170,25 +157,12 @@ export function ModelPicker({
|
|||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open ? (
|
||||
<motion.div
|
||||
ref={panel}
|
||||
initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 6, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 6, scale: 0.98 }}
|
||||
transition={reduceMotion ? { duration: 0 } : { duration: 0.16, ease: "easeOut" }}
|
||||
className="glass-panel absolute bottom-full right-0 z-40 mb-2 w-80 p-3"
|
||||
role="dialog"
|
||||
aria-label={t("chat.modelTitle")}
|
||||
>
|
||||
<h2 className="px-1 pb-2 text-[13px] font-semibold uppercase tracking-wide text-[var(--text-subtle)]">
|
||||
{t("chat.modelTitle")}
|
||||
</h2>
|
||||
{rows}
|
||||
<p className="px-1 pt-2 text-[11.5px] text-[var(--text-subtle)]">{t("chat.modelNote")}</p>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<PickerPopover open={open} onClose={onClose} label={t("chat.modelTitle")} align="right" width="w-80">
|
||||
<h2 className="px-1 pb-2 text-[13px] font-semibold uppercase tracking-wide text-[var(--text-subtle)]">
|
||||
{t("chat.modelTitle")}
|
||||
</h2>
|
||||
{rows("none")}
|
||||
<p className="px-1 pt-2 text-[11.5px] text-[var(--text-subtle)]">{t("chat.modelNote")}</p>
|
||||
</PickerPopover>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue