diff --git a/docs/CHAT-TYPOGRAPHY.md b/docs/CHAT-TYPOGRAPHY.md index aea80b8..81f62d3 100644 --- a/docs/CHAT-TYPOGRAPHY.md +++ b/docs/CHAT-TYPOGRAPHY.md @@ -180,6 +180,27 @@ transcript collapses into one undifferentiated column, the opposite of the inten height back in `em`, so it tracks D2's prose size. Net: **20.5px reclaimed per message** while the turn-boundary-to-continuation ratio *improves* from 1.5× to 2.2×. +**Corrected after shipping: the clipped label needs an anchor.** T4 measured the label's *box* and +never asked *where it was laid out*. The clipping pattern takes it out of flow with +`position: absolute`, and nothing above it was positioned — so every label was laid out against the +page rather than the transcript, at its unscrolled place in the log, out of reach of the log's +scrolling and clipping. A label that started more than a viewport down sat below the fold, the page +grew to reach that one pixel, and the page then scrolled: once the log was at its end, the wheel +moved the whole shell — transcript, composer and status row — up over blank space. It showed on any +conversation past its first long turn. + +`#log { position: relative }` makes the log the containing block for everything inside it: this +label, and anything out of flow added later. Measured the same way (Chromium, the shipped +`chat.html`, two turns, 1118px viewport): + +| | T4 as shipped | with the anchor | +| --- | --- | --- | +| label's `offsetParent` | `
` | **`#log`** | +| the last label, its message scrolled to −22px | at 1962px: pinned to the page | **at −23px: 1px above its message** | +| page height | 1963px (that label, plus its pixel) | **1118px** (the viewport) | +| composer after 4 wheel ticks at the end of the log | 400px higher, blank page beneath | **unmoved** | +| transcript content height | 2919px | 2919px (nothing inside the log moves) | + ### D7 — It stays hackable: two settings, no hard-coded values. `levelcode.ai.chat.proseWidth` (px) and `levelcode.ai.chat.fontSize` (px). Both flow through the CSS diff --git a/extensions/levelcode-ai/media/chat.html b/extensions/levelcode-ai/media/chat.html index 122716d..4a125c1 100644 --- a/extensions/levelcode-ai/media/chat.html +++ b/extensions/levelcode-ai/media/chat.html @@ -143,6 +143,17 @@ status row resolve the same values the transcript does. `--shell-x` is the log's horizontal padding AND the shell column's inset, which is what keeps the two edges on the same line. */ #log { flex: 1; overflow-y: auto; padding: 12px var(--shell-x); display: flex; flex-direction: column; gap: 12px; } + /* THE LOG CONTAINS WHAT IS IN IT. The page is a 100vh shell and this is the only thing in it that + scrolls, so what sits below it — the composer, the status row — never moves. That holds only while + nothing in the transcript is laid out against the PAGE, and an out-of-flow box with no positioned + ancestor is: it sits at its unscrolled place in the transcript, where neither this element's + scrolling nor its clipping reaches it. `.msg .role` (below) is such a box. Every labelled message + that starts more than a viewport down the transcript — in practice, every turn after a long first + one — put its label below the fold; the page grew to reach that one pixel, and the wheel, once the + log was at its end, scrolled the whole shell away: transcript, composer and status row riding up + over a blank page. Positioned, the log is the containing block for every descendant: the label, + and whatever is added later. webviewCss.test.js fails if this goes. */ + #log { position: relative; } /* Log is a flex column → children default to flex-shrink:1. Any child with overflow:hidden (e.g. the approval/edit cards) then gets an auto min-size of 0 and the flex algorithm crushes it to an invisible line once the log overflows. Pin every child to its natural height; the log itself scrolls instead. */ @@ -241,7 +252,11 @@ wall of text. So the element stays in the accessibility tree and leaves the visual layer. This is why the rule below must never be "simplified" to `display: none` or `visibility: hidden` — both take it out of the a11y tree too, which is the whole thing we are preserving. webviewCss.test.js - fails on either. */ + fails on either. + + ABSOLUTE NEEDS AN ANCHOR. Taking the label out of flow is what stops it reserving a line — and it + also means the label is laid out against its nearest positioned ancestor. That is `#log` (see + `#log { position: relative }` above). Without one it is the page, and the page scrolls. */ .msg .role { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; diff --git a/extensions/levelcode-ai/test/webviewCss.test.js b/extensions/levelcode-ai/test/webviewCss.test.js index 5c4056a..4a1eef6 100644 --- a/extensions/levelcode-ai/test/webviewCss.test.js +++ b/extensions/levelcode-ai/test/webviewCss.test.js @@ -516,6 +516,42 @@ test('TRANSCRIPT: the speaker label leaves the screen but NOT the accessibility assert.match(add, /cont \? '' : '