fix(dashboard): stop Chat Latest button jumping under the cursor

Center the jump-to-latest chip with margin-inline auto instead of
transform:translateX(-50%) so global .btn transform transitions and
:active scale cannot shift it sideways in Quick Chat and full Chat.
This commit is contained in:
gsxdsm
2026-07-23 18:15:03 -07:00
parent 716e698628
commit b67dca4178
3 changed files with 67 additions and 12 deletions

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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*:/);
}
});
});