FN-6212: cap chat input height on tablets
Limit the chat composer growth on tablet viewports while preserving desktop autosizing. - Add a tablet-specific 200px max height for the chat input textarea. - Apply the same cap in ChatView autosize calculations when tablet mode is active. - Generalize chat input height helpers and cover the tablet cap in autosize tests. Files changed: packages/dashboard/app/components/ChatView.css | 9 +++++++++ packages/dashboard/app/components/ChatView.tsx | 20 +++++++++++++------- packages/dashboard/app/components/QuickChatFAB.tsx | 4 ++-- .../__tests__/ChatView.chat-input-autosize.test.tsx | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-6212 Fusion-Task-Lineage: b11cd658-8289-4795-8e39-7d03a1d51d41
This commit is contained in:
@@ -1476,6 +1476,15 @@
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* FN-6212: On tablet viewports (769–1024px), cap the composer to 200px
|
||||
so it doesn't consume most of the visible thread. Desktop keeps 640px;
|
||||
mobile is already flex-constrained by the chat-thread layout. */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.chat-input-textarea {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
|
||||
@@ -60,19 +60,23 @@ export interface ChatViewProps {
|
||||
// Keep a generous cap so pasted multi-paragraph text stays visible while
|
||||
// still preventing the composer from overtaking the message pane on short viewports.
|
||||
const CHAT_INPUT_MAX_HEIGHT_PX = 640;
|
||||
const TABLET_INPUT_MAX_HEIGHT_PX = 200;
|
||||
/** Canonical definition lives in packages/dashboard/src/chat.ts (ROOM_SKIP_SENTINEL). */
|
||||
const ROOM_SKIP_SENTINEL = "__SKIP__";
|
||||
let chatViewWasPreviouslyInactive = false;
|
||||
|
||||
export function resolveChatInputOverflowY(scrollHeight: number): "auto" | "hidden" {
|
||||
return scrollHeight > CHAT_INPUT_MAX_HEIGHT_PX ? "auto" : "hidden";
|
||||
export function resolveChatInputOverflowY(
|
||||
scrollHeight: number,
|
||||
maxHeight: number = CHAT_INPUT_MAX_HEIGHT_PX,
|
||||
): "auto" | "hidden" {
|
||||
return scrollHeight > maxHeight ? "auto" : "hidden";
|
||||
}
|
||||
|
||||
export function clampChatInputHeight(scrollHeight: number): number {
|
||||
export function clampChatInputHeight(scrollHeight: number, maxHeight: number = CHAT_INPUT_MAX_HEIGHT_PX): number {
|
||||
// Floor matches QuickChat (clampQuickChatInputHeight) and the CSS min-height,
|
||||
// so a 0-scrollHeight measurement (e.g. before layout) still yields a
|
||||
// sensible inline height instead of collapsing the composer to 0.
|
||||
return Math.max(40, Math.min(scrollHeight, CHAT_INPUT_MAX_HEIGHT_PX));
|
||||
return Math.max(40, Math.min(scrollHeight, maxHeight));
|
||||
}
|
||||
|
||||
function formatRelativeTime(dateStr: string, t: TFunction<"app">): string {
|
||||
@@ -1863,10 +1867,12 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveMax = mode === "tablet" ? TABLET_INPUT_MAX_HEIGHT_PX : CHAT_INPUT_MAX_HEIGHT_PX;
|
||||
|
||||
composer.style.height = "auto";
|
||||
composer.style.height = `${clampChatInputHeight(composer.scrollHeight)}px`;
|
||||
composer.style.overflowY = resolveChatInputOverflowY(composer.scrollHeight);
|
||||
}, []);
|
||||
composer.style.height = `${clampChatInputHeight(composer.scrollHeight, effectiveMax)}px`;
|
||||
composer.style.overflowY = resolveChatInputOverflowY(composer.scrollHeight, effectiveMax);
|
||||
}, [mode]);
|
||||
|
||||
const handleComposerRef = useCallback((textarea: HTMLTextAreaElement | null) => {
|
||||
inputRef.current = textarea;
|
||||
|
||||
@@ -109,10 +109,10 @@ function formatModelTagName(modelInfo: ModelInfo | null, parsedSelection: Parsed
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function clampQuickChatInputHeight(scrollHeight: number): number {
|
||||
export function clampQuickChatInputHeight(scrollHeight: number, maxHeight: number = 640): number {
|
||||
// Match ChatView's 640px cap so pasted multi-paragraph text remains visible,
|
||||
// while keeping an upper bound that protects message visibility on short screens.
|
||||
return Math.max(40, Math.min(scrollHeight, 640));
|
||||
return Math.max(40, Math.min(scrollHeight, maxHeight));
|
||||
}
|
||||
|
||||
function truncateToolValue(value: string, maxLength: number): string {
|
||||
|
||||
@@ -33,9 +33,19 @@ describe("ChatView chat input autosize", () => {
|
||||
expect(stopRule?.[0]).toContain("min-height: var(--chat-input-control-size)");
|
||||
});
|
||||
|
||||
it("caps textarea max-height at 200px on tablet viewports", () => {
|
||||
const tabletRule = chatViewCss.match(
|
||||
/@media \(min-width: 769px\) and \(max-width: 1024px\)\s*\{\s*\.chat-input-textarea\s*\{[^}]*\}\s*\}/,
|
||||
);
|
||||
|
||||
expect(tabletRule).not.toBeNull();
|
||||
expect(tabletRule?.[0]).toContain("max-height: 200px");
|
||||
});
|
||||
|
||||
it("clamps oversized textarea growth to the new max height", () => {
|
||||
expect(clampChatInputHeight(600)).toBe(600);
|
||||
expect(clampChatInputHeight(800)).toBe(640);
|
||||
expect(clampChatInputHeight(800, 200)).toBe(200);
|
||||
expect(clampChatInputHeight(600)).not.toBe(120);
|
||||
});
|
||||
|
||||
@@ -45,7 +55,11 @@ describe("ChatView chat input autosize", () => {
|
||||
|
||||
it("keeps overflow hidden until content exceeds the max height cap", () => {
|
||||
expect(resolveChatInputOverflowY(80)).toBe("hidden");
|
||||
expect(resolveChatInputOverflowY(200)).toBe("hidden");
|
||||
expect(resolveChatInputOverflowY(201)).toBe("hidden");
|
||||
expect(resolveChatInputOverflowY(640)).toBe("hidden");
|
||||
expect(resolveChatInputOverflowY(641)).toBe("auto");
|
||||
expect(resolveChatInputOverflowY(200, 200)).toBe("hidden");
|
||||
expect(resolveChatInputOverflowY(201, 200)).toBe("auto");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user