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:
Martin Dvořák 2026-10-18 21:55:47 +00:00
commit 705808d45a
7 changed files with 215 additions and 82 deletions

View file

@ -1513,6 +1513,22 @@ Things I chose while building, with the reasoning:
into view as you type. No strings, so no i18n; the additive change sits on top of the merged
scene-heat rework unchanged.
111. **The model dropdown was clipped by the card it lived in.** "The model selection in teach it
back is kinda broken at least on desktop": the pickers' desktop panels were
`position: absolute` children of whatever container held their chip. In chat that is the
composer at the bottom of the column (open upwards, nothing clips it), but Teach it back puts
its chips inside a scrolling form card — so the panel opened upward, out of the card, and its
first rows (the selected model among them) were cut off by the viewport: the probe measured
`top: -66px`, and the list could not be reached by scrolling the inner list. Both pickers now
hand the desktop surface to `PickerPopover`: portalled to `<body>`, `position: fixed`, anchored
to the trigger that opened it (`[data-picker-trigger][aria-expanded="true"]`), opening on the
side with room and capped to the viewport, so the panel scrolls as a whole and always keeps its
heading and note attached to the chip. It closes on an outside press, Escape, a resize or a
page scroll (not on scrolling its own list), and the phone sheets are untouched. Pinned by the
teach suite: the panel's rect sits fully inside the viewport, `elementFromPoint` over it hits
the panel, it is not a descendant of the scrolling card, and picking a model updates the chip
and `freezmo.chat.model.v1` (46 checks).
110. **The reminder is written by the browser, because the notes never leave it.** Push reminders
used to say one generic line to everyone, and the server had no way to do better: it only
knows push endpoints, never a deck. So the notification text is now composed on the device