ui: the reading field's phantom scrollbar — the 1px font-box surplus

Empirically diagnosed via live browser measurement: Source Sans 3's
font box is 22px at 16px, but lh-input's 1.25 leading gives a 20px line
box - the font's content area sticks 1px below the line, Chromium
counts it in scrollHeight, and the zero-padding field-sizing textarea
turned it into a permanent 1px scroll: the bar showed EMPTY and at
every expansion size below max-h (owner report). The composer and dock
fields never showed it because their py-2 absorbs the surplus
implicitly. Fix: py-0.5 on the reading field - the same absorption,
line-height untouched (the caret law, typography round 2), and the
max-h-40 cap still scrolls honestly past 160px of text.
This commit is contained in:
amosblomqvist
2026-10-08 19:25:47 +03:00
parent d790384b72
commit 4355667d96
2 changed files with 33 additions and 1 deletions
+10 -1
View File
@@ -57,6 +57,15 @@ export function ReadingThinkingField({
<textarea <textarea
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
/* py-0.5 (owner round, 2026-10-07): the scrollbar fix. Source
Sans 3's font box is 22px at 16px; lh-input's 1.25 leading
gives a 20px line box — the content area sticks 1px below the
line, Chromium counts it in scrollHeight, and a zero-padding
textarea turns that into a permanent 1px scroll (the bar shows
empty and at every size below max-h). 2px of vertical padding
absorbs it — the same absorption the composer and dock fields
get implicitly from their py-2. Line-height stays 1.25 (the
caret law, typography round 2). */
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Escape") { if (e.key === "Escape") {
// Esc stops dictation (the strip's rule) — claimed so the // Esc stops dictation (the strip's rule) — claimed so the
@@ -87,7 +96,7 @@ export function ReadingThinkingField({
}} }}
placeholder="Whatever comes to mind as you read." placeholder="Whatever comes to mind as you read."
rows={1} rows={1}
className="field-sizing-content lh-input max-h-40 w-full resize-none bg-transparent outline-none placeholder:text-muted-foreground/70" className="field-sizing-content lh-input max-h-40 w-full resize-none bg-transparent py-0.5 outline-none placeholder:text-muted-foreground/70"
/> />
</div> </div>
); );
+23
View File
@@ -0,0 +1,23 @@
import React from "react";
(globalThis as Record<string, unknown>).React = React;
import { renderToStaticMarkup } from "react-dom/server";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { ReadingThinkingField } from "../components/reading-thinking.tsx";
describe("the reading-thinking field (owner round, 2026-10-07)", () => {
it("carries vertical padding — the 1px-scrollbar absorption", () => {
const html = renderToStaticMarkup(
<ReadingThinkingField value="" onChange={() => {}} claim={false} />,
);
// The fix (empirically diagnosed): Source Sans 3's font box (22px at
// 16px) exceeds lh-input's 20px line box by 1px; Chromium counts that
// in scrollHeight and a zero-padding field-sizing textarea becomes a
// permanent 1px scroll (a bar shown empty AND at every expansion below
// max-h). py-0.5 absorbs it — the composer/dock fields get the same
// absorption implicitly from py-2. Line-height stays 1.25 (caret law).
assert.ok(html.includes("py-0.5"), "the vertical padding is on the textarea");
// The cap survives: growth past max-h-40 still scrolls honestly.
assert.ok(html.includes("max-h-40"), "the 160px growth cap stays");
});
});