From d7badf1512008ea8bc46d24368df72788a817291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 30 Jul 2026 15:59:36 +0200 Subject: [PATCH] fix(coach): keep the composer one row tall until someone types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a phone the composer stacked its controls under the textarea, so an EMPTY input was two rows tall: a text line, a 44 px tap-target row, and padding. That is roughly a tenth of a phone screen spent before anyone has typed anything, and it is the first thing on the screen when the Coach opens. The auto-grow was never the problem. The textarea already starts at one line and grows to six; it was the layout around it that reserved the second row up front. The stacking existed to protect keyboard focus order. That argument does not hold: focus follows DOM order, and the DOM order is textarea, actions, Send in both layouts. Nothing jumps. `items-end` keeps the controls beside the last line as the textarea grows, so the send button stays where the thumb left it. One component serves both surfaces — the conversation view and the hero, which re-parents the same live composer — so this is one fix rather than two. The test that asserted the stacked layout now asserts the row, with the reason written down, and it fails when the stack comes back. --- .../__tests__/coach-input.test.tsx | 20 ++++++++++++++----- .../insights/coach-panel/coach-input.tsx | 16 +++++++++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/components/insights/coach-panel/__tests__/coach-input.test.tsx b/src/components/insights/coach-panel/__tests__/coach-input.test.tsx index 891d6264c..1ee7ef2c9 100644 --- a/src/components/insights/coach-panel/__tests__/coach-input.test.tsx +++ b/src/components/insights/coach-panel/__tests__/coach-input.test.tsx @@ -228,7 +228,7 @@ describe("", () => { expect(html).toContain('data-slot="coach-input-send"'); }); - it("puts the textarea above a split control row on phones", () => { + it("keeps the textarea and its controls on one row at every width", () => { const html = render( ", () => { const controls = html.match( /]*data-slot="coach-input-controls"[^>]*>/, ); - expect(controls?.[0]).toMatch(/\bw-full\b/); - expect(controls?.[0]).toMatch(/\bjustify-between\b/); - expect(controls?.[0]).toMatch(/\bsm:w-auto\b/); - expect(controls?.[0]).not.toMatch(/\bsm:contents\b/); + // The controls sit BESIDE the textarea, not under it. Stacking them made an + // empty composer two rows tall — a text line plus a 44 px tap-target row — + // which is roughly a tenth of a phone screen before anyone has typed. The + // controls must therefore never claim the full width or push themselves + // onto their own line. + expect(controls?.[0]).not.toMatch(/\bw-full\b/); + expect(controls?.[0]).not.toMatch(/\bjustify-between\b/); + expect(controls?.[0]).toMatch(/\bshrink-0\b/); + + // And the row itself is a row at every width, bottom-aligned so the send + // button stays beside the last line as the textarea grows. + const shell = html.match(/]*class="[^"]*\bflex-row\b[^"]*"[^>]*>/); + expect(shell?.[0]).toMatch(/\bitems-end\b/); + expect(shell?.[0]).not.toMatch(/\bflex-col\b/); }); it("sizes the hub actions trigger to the 44px tap-target floor on phones", () => { diff --git a/src/components/insights/coach-panel/coach-input.tsx b/src/components/insights/coach-panel/coach-input.tsx index 123e4f97c..a17d49b7e 100644 --- a/src/components/insights/coach-panel/coach-input.tsx +++ b/src/components/insights/coach-panel/coach-input.tsx @@ -419,9 +419,17 @@ export function CoachInput({ "border-border/60 bg-muted/40 group rounded-2xl border", "shadow-sm transition-colors", "focus-within:border-primary/50 focus-within:ring-primary/50 focus-within:bg-background focus-within:ring-2", - // Mobile uses two rows. Desktop keeps the same DOM and visual order - // (textarea, actions, Send) so keyboard focus never jumps backwards. - "flex flex-col gap-1.5 p-1.5 sm:flex-row sm:items-end", + // One row at every width. Mobile used to stack the controls under the + // textarea, which made an EMPTY composer two rows tall: a 44 px line + // plus a 44 px touch-target row plus padding, around a tenth of a + // phone screen before anyone had typed anything. The stacking was + // there to protect keyboard focus order, but that argument does not + // hold: focus follows DOM order, and the DOM order (textarea, + // actions, Send) is identical in both layouts. Nothing jumps. + // + // `items-end` keeps the controls beside the LAST line as the textarea + // grows, so the send button stays where the thumb left it. + "flex flex-row items-end gap-1.5 p-1.5", )} >