diff --git a/CHANGELOG.md b/CHANGELOG.md index 421264a..64092df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,36 @@ for 1.0.0 have something to be assembled from. ## Unreleased +## 1.1.2 + +Two things a phone found that 1.1.0's phone pass had not. + +- Fixed: **the administration area could not be navigated on a phone.** Admin + has a nav of its own rather than the chat sidebar, and 1.1.0 gave every + sidebar the drawer behaviour — starts closed, slides in — without giving that + one any of the drawer's furniture. So it sat off-screen with no button to open + it, no close, and nothing to tap beside it: every administration page was + reachable and then a dead end. It now opens, closes and dims the page like the + other one, and a test refuses any future sidebar that cannot be opened. +- Fixed: **the chat gave nearly a quarter of a phone screen to margins**, so + anything that could not wrap had to be scrolled to sideways. The thread's side + padding is halved, and the speaker's avatar moves above the turn instead of + sitting in a 44px column beside every line of it — a code block gained about + sixty pixels of readable width. +- Fixed: **the chat's title was squeezed to nothing.** The row's designated + shrinker is hidden below a tablet width, so on a phone the controls went rigid + and asked for 317 pixels of a 390 pixel bar; the heading was not truncated, it + simply stopped occupying space. The model picker gives now, and on a phone it + shows its avatar rather than its name — the name is one tap away and the + title is not. +- Tick boxes and the smaller buttons are big enough to hit on a phone. A + checkbox is drawn by the browser at about sixteen pixels whatever the type + around it, which made it the smallest target in the application by some way, + and the admin lists are mostly checkboxes. +- Fixed: **icon buttons could be squashed below their own size.** The sidebar + toggle measured eighteen pixels across on a phone, under half its target, + because a full row shrank the button rather than the text beside it. + ## 1.1.1 One bug, and it is the one that made 1.1.0 look broken the moment you updated to diff --git a/scripts/shoot.py b/scripts/shoot.py index 1aa41b5..f233911 100644 --- a/scripts/shoot.py +++ b/scripts/shoot.py @@ -29,10 +29,19 @@ import tempfile from pathlib import Path REPO = Path(__file__).resolve().parent.parent -SRC = REPO / "src" -sys.path.insert(0, str(SRC)) +sys.path.insert(0, str(REPO / "src")) -STATIC = SRC / "lembas/web/static" +# Resolved from the package that actually got imported, not from where this +# file happens to sit. A copy of this script run from somewhere else silently +# pointed STATIC at a directory that did not exist, every asset URL was +# rewritten to a file:// path with nothing behind it, and the run measured an +# unstyled document -- reporting that every page in the application overflowed +# by thirty thousand pixels. The guard below only asked whether the URLs had +# been rewritten, which they had. +import lembas # noqa: E402 + +SRC = Path(lembas.__file__).resolve().parent.parent +STATIC = Path(lembas.__file__).resolve().parent / "web/static" CHROMIUM = shutil.which("chromium") or shutil.which("chromium-browser") # Routes that are served by the app rather than mounted, so the rewrite has to @@ -131,8 +140,17 @@ window.__measure = function () { tallCulprits: culprits('y'), wideCulprits: culprits('x'), /* The invariant: the application shell fills the window and the DOCUMENT - never scrolls. A document taller than the window is the /settings bug. */ - documentScrolls: de.scrollHeight > window.innerHeight + 1, + never scrolls *for the reader*. A document taller than the window is the + /settings bug -- but only when the reader can actually move it. `overflow: + hidden` blocks a wheel and a finger while still permitting an assignment + to scrollTop, so a page whose shell clips a tall descendant reports a + scrollHeight of thousands and scrolls for nobody. /admin/prompts does + exactly that, and reading the raw height called it a bug four times. */ + documentScrolls: + de.scrollHeight > window.innerHeight + 1 && + ["visible", "auto", "scroll"].indexOf( + getComputedStyle(document.documentElement).overflowY + ) !== -1, scrollsSideways: de.scrollWidth > window.innerWidth + 1, smallTargets: small.slice(0, 40), smallCount: small.length, @@ -219,6 +237,18 @@ def rewrite(html: str, client, assets: Path) -> str: f"{sorted(set(blocking))[:8]}" ) + # And that what they were rewritten *to* is really there. A rewrite that + # matches and produces a dead path is indistinguishable, from inside the + # browser, from no stylesheet at all -- and it is the failure that actually + # happened, twice. + missing = [ + url + for url in re.findall(r'(?:href|src)="file://([^"?]+)"', html) + if not Path(url).exists() + ] + if missing: + raise SystemExit(f"REWRITTEN TO NOTHING -- still an unstyled document: {missing[:5]}") + # The one-time notifications offer is a modal over the very page we came # to measure, and it is gated on a localStorage key. Set it in the head, so # it runs before the deferred script that reads it. diff --git a/src/lembas/__init__.py b/src/lembas/__init__.py index 5ceab52..f7b6d4c 100644 --- a/src/lembas/__init__.py +++ b/src/lembas/__init__.py @@ -1,3 +1,3 @@ """LLeMbas - a Middle-earth themed web UI for OpenAI-compatible LLM endpoints.""" -__version__ = "1.1.1" +__version__ = "1.1.2" diff --git a/src/lembas/web/static/css/app.css b/src/lembas/web/static/css/app.css index 3658a58..c85c06c 100644 --- a/src/lembas/web/static/css/app.css +++ b/src/lembas/web/static/css/app.css @@ -185,6 +185,12 @@ button, input, textarea, select { /* Square, and the same height as everything beside it. */ .btn--icon { width: var(--control-h); + /* Square, and it stays square. Without this a flex row that runs out of room + shrinks it instead of its neighbours -- the sidebar toggle measured 18px + across on a 390px chat, less than half the target it is supposed to be, + while the row beside it kept every pixel it had asked for. A control's + size is not the give in a layout; text is. */ + flex: none; padding: 0; background: transparent; border-color: transparent; @@ -354,12 +360,28 @@ button, input, textarea, select { } .checkbox input { accent-color: var(--accent); - width: 1rem; - height: 1rem; + width: var(--check-size); + height: var(--check-size); flex: none; cursor: pointer; } +/* Every tick box, not only the ones inside a `.checkbox` label -- the admin + lists put bare ones in a row and those were 16px square on a phone. */ +input[type="checkbox"], +input[type="radio"] { + accent-color: var(--accent); + width: var(--check-size); + height: var(--check-size); +} +/* Except the ones that are deliberately 1px: a visually-hidden radio is the + state behind a label, and the label is the target. */ +input.visually-hidden[type="radio"], +input.visually-hidden[type="checkbox"] { + width: 1px; + height: 1px; +} + /* Multi-column form layout, one definition. */ .grid { display: grid; gap: var(--sp-4); } .grid--2 { grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); } @@ -953,7 +975,48 @@ body.is-resizing .canvas__body { pointer-events: none; } min-width: 0; flex: 1; } -.topbar__actions { display: flex; align-items: center; gap: var(--sp-2); flex: none; } +/* + The controls on the right of the topbar. + + `flex: none` on the group with `min-width: 0` inside it: the group keeps the + width its controls need, and the one child whose width is a *name* rather + than a control -- the model picker -- is the thing allowed to give. Without + the second half the group asked for 317px of a 390px bar and the chat's + title, which is `flex: 1`, was squeezed to exactly zero: a heading that had + not been shortened or truncated but had simply ceased to occupy space. +*/ +.topbar__actions { + display: flex; + align-items: center; + gap: var(--sp-2); + /* Allowed to give, which it was not. `--topbar__where` used to be the + designated shrinker in this row, and it is `display: none` below 64rem -- + so on a phone the group became rigid, asked for 317px of a 390px bar, and + the title (`flex: 1`) was squeezed to exactly zero: a heading that had not + been truncated but had ceased to occupy space. + + Nothing inside it shrinks except the model picker: every button here is + `flex: none` because a control's size is not the give in a layout. */ + flex: 0 1 auto; + min-width: 0; +} +/* A title identifies the page, so it gets a floor and truncates rather than + disappearing. */ +.topbar__title { min-width: 4rem; } + +/* The one control in this row whose width is somebody else's decision -- a + model's label is whatever an administrator called it -- so it is the one + that gives, and it gives by truncating its name rather than its avatar or + its chevron. */ +.topbar__actions .picker { min-width: 0; } +.topbar__actions .picker__button { max-width: 100%; } +.topbar__actions .picker__label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.picker__avatar, .picker__chevron { flex: none; } /* Which machine an agent chat runs on, and where. @@ -1243,6 +1306,22 @@ body.is-resizing .canvas__body { pointer-events: none; } } @media (max-width: 48rem) { + /* The bar is the densest row in the application and the one with the least + room: a toggle, a title, a model, and up to four panel buttons. Tighter + padding and a smaller gap buy back about 24px, which is the difference + between a title that truncates and one there is no room for at all. */ + .topbar { + gap: var(--sp-2); + padding-right: max(var(--sp-2), var(--safe-right)); + padding-left: max(var(--sp-2), var(--safe-left)); + } + + /* The model's name costs about a hundred pixels and its avatar does not, + and the picker opens onto a list of full names the moment it is touched. + So on a phone the avatar carries the identity and the chat's own title -- + which nothing else on the screen tells you -- gets the room back. */ + .topbar__actions .picker__label { display: none; } + .sidebar { position: fixed; inset: 0 auto 0 0; diff --git a/src/lembas/web/static/css/chat.css b/src/lembas/web/static/css/chat.css index fd80060..419f7f0 100644 --- a/src/lembas/web/static/css/chat.css +++ b/src/lembas/web/static/css/chat.css @@ -1726,3 +1726,52 @@ .thread__intro > * { animation: intro-rise var(--dur-3) var(--ease-out) both; } .thread__intro > *:nth-child(2) { animation-delay: 60ms; } .thread__intro > *:nth-child(3) { animation-delay: 120ms; } + +/* + --- A phone ---------------------------------------------------------------- + + The one width-aware block in this file, and the reason the blanket ban on + `@media` here was lifted: everything below is a *size*, and there is no + intrinsic-sizing trick that makes 24px of thread padding the right amount on + a 390px screen. The ban existed to stop the composer toolbar being "fixed" + with a breakpoint instead of by saying which child gives, and that guarantee + is asserted directly now (`tests/test_chat.py`) -- so this block may not touch + `.composer__toolbar` or `.composer__actions`, and a test refuses it if it + does. + + What was wrong: a 390px screen spent 40px of its width on thread padding and + another 44 on the avatar gutter before a single word was drawn, which is + nearly a quarter of the screen given over to margin -- so anything that could + not wrap had to be scrolled to sideways. +*/ +@media (max-width: 48rem) { + /* Half the horizontal padding. The vertical stays: it is what separates one + turn from the next, and turns are no closer together on a phone. */ + .thread { + padding-left: var(--sp-3); + padding-right: var(--sp-3); + } + + /* The avatar goes to the top of the turn rather than beside it, so the body + gets the whole width. The gutter is what identifies the speaker and it + still does; it simply stops costing 44px of every line. */ + .msg { + grid-template-columns: 1fr; + gap: var(--sp-2); + } + .msg__gutter { + width: var(--control-h-sm); + height: var(--control-h-sm); + } + .msg__meta { gap: var(--sp-2); } + + /* A bubble against the edge of the screen wants less inside it. */ + .msg--user .msg__body--plain { padding: var(--sp-2) var(--sp-3); } + + /* The composer is the other thing pressed against both edges. */ + .composer { padding-left: var(--sp-2); padding-right: var(--sp-2); } + + /* A hint that runs to four lines on a phone is a hint nobody reads, and it + sits directly under the thing a thumb is reaching for. */ + .composer__hint { font-size: var(--text-xs); } +} diff --git a/src/lembas/web/static/css/tokens.css b/src/lembas/web/static/css/tokens.css index 08699a7..e18f3e5 100644 --- a/src/lembas/web/static/css/tokens.css +++ b/src/lembas/web/static/css/tokens.css @@ -145,6 +145,9 @@ Raising the token is the only version that reaches all of them, and it is what `--control-h` exists for. */ --tap-min: 2.75rem; + /* A tick box, which does not take its size from `--control-h`: the browser + draws it and only `width`/`height` move it. */ + --check-size: 1rem; /* --- The window's own edges --------------------------------------------- Installed on a phone, the page runs under the notch and the home @@ -421,9 +424,16 @@ @media (pointer: coarse), (max-width: 48rem) { :root { --control-h: var(--tap-min); - --control-h-sm: 2.25rem; + /* 40px, not the 36 a comfortable pointer gets. A `.btn--sm` is a secondary + action, not an unimportant one -- Edit, Enable and Use default are all + `.btn--sm`, and on a phone they are the whole interaction. */ + --control-h-sm: 2.5rem; --control-px: var(--sp-4); --control-px-sm: var(--sp-3); + /* A native checkbox is 13-16px whatever the surrounding type is, and no + amount of padding on its label changes the box itself. It is the + smallest target in the application on a phone by some margin. */ + --check-size: 1.375rem; } } diff --git a/src/lembas/web/static/img/screenshot-narrow.png b/src/lembas/web/static/img/screenshot-narrow.png index 6f6f7fa..5554a5a 100644 Binary files a/src/lembas/web/static/img/screenshot-narrow.png and b/src/lembas/web/static/img/screenshot-narrow.png differ diff --git a/src/lembas/web/templates/admin/_layout.html b/src/lembas/web/templates/admin/_layout.html index 3017ee0..f8827c4 100644 --- a/src/lembas/web/templates/admin/_layout.html +++ b/src/lembas/web/templates/admin/_layout.html @@ -14,10 +14,20 @@ {% block body %}