A model menu that stays on the phone

Inside a chat the picker sits mid-bar with the panel buttons to its right,
and its menu opened from the picker's right edge -- at 390px it spanned
x = -132..226, cutting every model's name off. Below 48rem the bar is now
the containing block and the menu is pinned between its edges (capped at
24rem). Opening it on a touchscreen no longer focuses the filter, which
raised the keyboard over half the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 20:36:45 +00:00
co-authored by Claude Opus 5.5
parent da43bc1459
commit 793f9c8cad
6 changed files with 75 additions and 1 deletions
+17
View File
@@ -16,6 +16,23 @@ for 1.0.0 have something to be assembled from.
## Unreleased
## 1.8.3
The model picker on a phone, which could not be read once a chat was open.
- **The model menu no longer runs off the left of the screen.** Inside a chat
the picker sits in the middle of the top bar, with the panel buttons to its
right, and its menu opened from the picker's right edge — so on a phone most
of it was off the screen and every model's name was cut off. On a narrow
screen the menu now hangs from the bar itself, edge to edge, and every name is
whole. Wider screens are unchanged.
- **Opening it on a touchscreen no longer raises the keyboard.** With more than
eight models the menu has a filter box, and it took the focus on opening — so
the keyboard came up and covered half the list you had opened it to choose
from. On a touchscreen the chosen model takes the focus instead; the filter is
one tap away. With a mouse, typing straight into the filter works as before.
## 1.8.2
The model lists, made readable. Both printed every capability switch as a tag —
+1 -1
View File
@@ -1,3 +1,3 @@
"""LLeMbas - a Middle-earth themed web UI for OpenAI-compatible LLM endpoints."""
__version__ = "1.8.2"
__version__ = "1.8.3"
+17
View File
@@ -1377,6 +1377,23 @@ body.is-resizing .canvas__body { pointer-events: none; }
which nothing else on the screen tells you -- gets the room back. */
.topbar__actions .picker__label { display: none; }
/* And the menu is the bar's, not the picker's. Anchored to the picker it
opens `right: 0` of a button that sits mid-bar with the panel buttons to
its right, so a 24rem menu ran off the left edge of a 390px phone and cut
every name in half. Taking `position` off the picker makes the bar the
containing block: the menu spans the bar under it, whatever sits where --
up to its usual 24rem, held at the bar's right edge by the auto margin. */
.topbar { position: relative; }
.topbar__actions .picker { position: static; }
.topbar__actions .picker__menu {
left: max(var(--sp-2), var(--safe-left));
right: max(var(--sp-2), var(--safe-right));
width: auto;
max-width: 24rem;
margin-left: auto;
}
.topbar__actions .picker__list { max-height: min(22rem, 60dvh); }
.sidebar {
position: fixed;
inset: 0 auto 0 0;
+5
View File
@@ -320,6 +320,11 @@
if (filter) {
filter.value = "";
applyFilter(menu, "");
}
// Not on a touchscreen: focusing a text field there raises the keyboard,
// which covers half the list the finger came to choose from. The filter
// is one tap away for whoever wants it.
if (filter && !window.matchMedia("(hover: none)").matches) {
filter.focus();
} else {
var selected = menu.querySelector(".picker__option.is-selected") ||
+16
View File
@@ -208,3 +208,19 @@ def test_the_desktop_minimum_is_still_declared():
for token in ("--terminal-width-min", "--canvas-width-min"):
assert f"{token}:" in TOKENS
assert f"var({token})" in APP_CSS
def test_the_topbar_model_menu_belongs_to_the_bar_on_a_phone():
"""1.8.3. Anchored to the picker, the menu opened `right: 0` of a button that
sits mid-bar with the panel buttons to its right, so on a 390px phone a 24rem
menu started 132px left of the screen and every model's name was cut off.
Below the phone breakpoint the picker gives up `position`, which makes the bar
the containing block, and the menu is pinned between the bar's two edges."""
body = _media_body(APP_CSS, "48rem")
assert re.search(r"\.topbar\s*\{\s*position:\s*relative", body)
assert re.search(r"\.topbar__actions \.picker\s*\{\s*position:\s*static", body)
menu = re.search(r"\.topbar__actions \.picker__menu\s*\{([^}]*)\}", body)
assert menu, "the topbar's menu is not placed on a phone"
for declaration in ("left:", "right:", "width: auto"):
assert declaration in menu.group(1), f"{declaration} missing from the phone menu"
+19
View File
@@ -111,3 +111,22 @@ def test_settings_lists_the_models_with_their_tags_below_the_name(client, models
assert listing.count("#i-eye") == 1
assert listing.count('class="model-list__more model-list__tags"') == 2
assert "tool_fetch" in listing
def test_opening_the_picker_on_a_touchscreen_does_not_raise_the_keyboard():
"""1.8.3. With more than eight models the menu has a filter, and `open()`
focused it -- which on a phone raises the keyboard over half the list the
finger came to choose from. The focus is gated on `(hover: none)`, the same
query the stylesheet uses for touch."""
from pathlib import Path
import lembas
js = (Path(lembas.__file__).parent / "web/static/js/ui.js").read_text(encoding="utf-8")
start = js.index("function open(picker)")
body = js[start : js.index("function applyFilter", start)]
assert "filter.focus()" in body, "the filter is no longer focused anywhere -- test is blind"
gated = r'if \(filter && !window\.matchMedia\("\(hover: none\)"\)\.matches\)\s*\{'
assert re.search(gated + r"\s*filter\.focus\(\)", body), (
"the filter is focused on open without asking whether this is a touchscreen"
)