diff --git a/.changeset/chat-jump-to-latest-active-transform.md b/.changeset/chat-jump-to-latest-active-transform.md index 004fd7a8a4..fa384738ef 100644 --- a/.changeset/chat-jump-to-latest-active-transform.md +++ b/.changeset/chat-jump-to-latest-active-transform.md @@ -2,6 +2,6 @@ "@runfusion/fusion": patch --- -summary: Fix the Chat View "Latest" button shifting sideways out from under the cursor when clicked. +summary: Stop the Chat/Quick Chat "Latest" button from jumping when the cursor moves near or presses it. category: fix -dev: `.chat-jump-to-latest:active` now composes `translateX(-50%) scale(0.97)` so the global `.btn:active` transform no longer replaces the centering transform. +dev: Center `.chat-jump-to-latest` with left/right + margin-inline auto instead of transform:translateX(-50%) so global `.btn` transform transitions and :active scale cannot shift the chip sideways. diff --git a/packages/dashboard/app/components/ChatView.css b/packages/dashboard/app/components/ChatView.css index acc1d0e95c..2780d01f8f 100644 --- a/packages/dashboard/app/components/ChatView.css +++ b/packages/dashboard/app/components/ChatView.css @@ -1035,20 +1035,21 @@ The thread-wide Markdown/plain render toggle (`.chat-thread-header-render-toggle word-break: break-word; } -.chat-jump-to-latest { - position: absolute; - left: 50%; - bottom: calc(var(--space-xl) * 3); - transform: translateX(-50%); - z-index: 2; -} - /* FNXC:ChatView 2026-07-22-00:00: The Latest button is centered with transform: translateX(-50%), so the global .btn:active { transform: scale(0.97) } rule must not replace that transform wholesale — doing so shifted the button half its width sideways on mousedown, out from under the cursor mid-click. Compose both transforms on :active instead (same fix as .devserver-log-viewer__new-logs-button). + +FNXC:ChatView 2026-07-23-18:15: +Compose-on-:active was still fragile: same specificity as `.btn:active` can lose the cascade war (SelectionCommentPopover regressed the same way), and the shared `.btn { transition: transform }` made any transform churn feel like the chip jumps when the cursor moves near/over it in Quick Chat and full Chat. Center with left/right + margin-inline auto + width: fit-content so positioning no longer owns `transform`. Global `.btn:active` scale then applies in place around the geometric center without a sideways teleport. Covers session + room jump buttons (shared class), floating Quick Chat, main Chat view, and mobile. */ -.chat-jump-to-latest:active { - transform: translateX(-50%) scale(0.97); +.chat-jump-to-latest { + position: absolute; + left: 0; + right: 0; + width: fit-content; + margin-inline: auto; + bottom: calc(var(--space-xl) * 3); + z-index: 2; } .chat-message--user { diff --git a/packages/dashboard/app/components/__tests__/ChatView.jump-to-latest.test.ts b/packages/dashboard/app/components/__tests__/ChatView.jump-to-latest.test.ts new file mode 100644 index 0000000000..7b4c70fb99 --- /dev/null +++ b/packages/dashboard/app/components/__tests__/ChatView.jump-to-latest.test.ts @@ -0,0 +1,54 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +/* +FNXC:ChatView 2026-07-23-18:15: +Regression guard for the Latest jump chip teleporting under the cursor. The control is shared by +main Chat, floating Quick Chat, and room threads (`.chat-jump-to-latest`). Centering must not use +`transform: translateX(-50%)` because global `.btn:active { transform: scale(0.97) }` and +`.btn { transition: transform }` replace or animate that translate and shift the chip sideways when +the pointer approaches or presses it. jsdom cannot hit-test the mid-press jump, so the invariant is +asserted on the stylesheet: every transform rule that targets the jump class is forbidden, and the +base rule must center with left/right + margin-inline auto + width: fit-content. +*/ +const chatViewCss = readFileSync(resolve(__dirname, "../ChatView.css"), "utf8"); + +function uncomment(css: string): string { + return css.replace(/\/\*[\s\S]*?\*\//g, ""); +} + +function collectJumpBlocks(css: string): Array<{ selector: string; block: string }> { + const blocks: Array<{ selector: string; block: string }> = []; + const rulePattern = /([^{}]+)\{([^{}]*)\}/g; + for (const match of uncomment(css).matchAll(rulePattern)) { + const selector = match[1].trim(); + if (selector.split(",").some((part) => part.trim().includes(".chat-jump-to-latest"))) { + blocks.push({ selector, block: match[2] }); + } + } + return blocks; +} + +describe("ChatView jump-to-latest centering", () => { + it("centers without transform so btn press/hover transitions cannot shift the chip sideways", () => { + const blocks = collectJumpBlocks(chatViewCss); + expect(blocks.length).toBeGreaterThanOrEqual(1); + + const base = blocks.find(({ selector }) => selector.trim() === ".chat-jump-to-latest"); + expect(base, "base .chat-jump-to-latest rule must exist").toBeDefined(); + expect(base!.block).toMatch(/left\s*:\s*0/); + expect(base!.block).toMatch(/right\s*:\s*0/); + expect(base!.block).toMatch(/margin-inline\s*:\s*auto/); + expect(base!.block).toMatch(/width\s*:\s*fit-content/); + expect(base!.block).not.toMatch(/transform\s*:/); + expect(base!.block).not.toMatch(/translateX\s*\(/); + + for (const { selector, block } of blocks) { + expect( + block, + `jump rule "${selector}" must not own transform (global .btn:active/hover transitions own it)`, + ).not.toMatch(/transform\s*:/); + } + }); +});