From 3fcae726c566d9937672e81b9829e0b41c6252ed Mon Sep 17 00:00:00 2001 From: Neko Date: Fri, 11 Sep 2026 02:36:18 +0800 Subject: [PATCH] fix(stage-layouts): center the mobile chat input (#2517) --- .../InteractiveArea.browser.test.ts | 30 +++++++++++++++++-- .../Layouts/MobileInteractiveArea.vue | 2 +- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.browser.test.ts b/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.browser.test.ts index 6141c54ca..eff0b5a90 100644 --- a/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.browser.test.ts +++ b/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.browser.test.ts @@ -182,6 +182,30 @@ describe('interactive area synchronized state', () => { await page.viewport(1280, 720) }) + it('centers the mobile textarea when no reply preview is visible', async () => { + // ROOT CAUSE: + // + // Without a reply preview, the 40px bubble has spare height around its + // 32px textarea and borders. Before the fix, justify-end put all spare + // height above the textarea: 6px above and 2px below. + // + // We fixed this with justify-center. The empty and single-line textarea + // now has 4px on each side, and multiline input stays centered. + await page.viewport(390, 844) + const { screen } = await renderArea(MobileInteractiveArea) + const bubble = screen.getByTestId('mobile-input-bubble').element() + const input = screen.getByRole('textbox') + + for (const draft of ['', 'Hello', 'First line\nSecond line', '']) { + await input.fill(draft) + await expect.poll(() => { + const outer = bubble.getBoundingClientRect() + const inner = input.element().getBoundingClientRect() + return Math.abs((inner.top - outer.top) - (outer.bottom - inner.bottom)) + }).toBeLessThanOrEqual(1) + } + }) + it('opens mobile settings from an icon-only header and restores focus', async () => { await page.viewport(390, 844) const { screen } = await renderArea(MobileInteractiveArea) @@ -440,9 +464,9 @@ describe('interactive area synchronized state', () => { await expect.poll(() => input.getBoundingClientRect().height).toBe(32) expect(send.getBoundingClientRect().height).toBe(32) expect(bubble.getBoundingClientRect().bottom).toBe(send.getBoundingClientRect().bottom) - expect(input.getBoundingClientRect().bottom).toBe( - bubble.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(bubble).borderBottomWidth), - ) + const bubbleBounds = bubble.getBoundingClientRect() + const inputBounds = input.getBoundingClientRect() + expect(inputBounds.top - bubbleBounds.top).toBe(bubbleBounds.bottom - inputBounds.bottom) }) it('closes mobile settings before requesting sign-in', async () => { diff --git a/packages/stage-layouts/src/components/Layouts/MobileInteractiveArea.vue b/packages/stage-layouts/src/components/Layouts/MobileInteractiveArea.vue index 2c106d096..c98692b22 100644 --- a/packages/stage-layouts/src/components/Layouts/MobileInteractiveArea.vue +++ b/packages/stage-layouts/src/components/Layouts/MobileInteractiveArea.vue @@ -566,7 +566,7 @@ onUnmounted(() => { data-testid="mobile-input-bubble" :data-dragging="inputBubbleDragging" :class="[ - 'group relative mx-auto min-h-10 flex flex-col justify-end origin-center overflow-hidden', + 'group relative mx-auto min-h-10 flex flex-col justify-center origin-center overflow-hidden', 'touch-none select-none focus-within:touch-auto focus-within:select-text', 'border-2 border-solid border-neutral-200/60 bg-neutral-100/80 backdrop-blur-md', 'dark:border-neutral-700/60 dark:bg-neutral-950/80',