unsloth/studio/frontend/src/hooks/use-collapse-scroll-lock.ts
Lee Jackson c20959dbf4
Studio: Improve chat composition, fix scroll behaviour, and refine sidebar UX (#5089)
* Chatbox, scroll, and menu fixes

- Fixed chatbox auto-expand height for multi-line text on the compare page
- Fixed chatbox UI to be consistent across compare and new chat
- Fixed scrolling being enabled on pages with no content, which also triggered the scroll-to-bottom button
- Fixed scroll-to-bottom button to only appear after scrolling up a reasonable amount instead of instantly
- Added shutdown studio button to the menu for easier access
- Fixed pop-up menu width to match the user button width

(cherry picked from commit cd4e390dfa84fe311fae79a781b96cc0ef5970a9)

* fix: correct compare scroll viewport and clean up chat composer UI polish

* Dark theme refactor and sidebar/chat UI refinements

- Complete refactoring of dark theme
- Replaced square rounded-corner user profile image with a circular bordered one
- Replaced user profile icon with 'U' initial and renamed label from 'Studio' to 'User'
- Chat bubbles now have a pointy top-right edge
- Sidebar menu tab line color selection is now consistent across all menus
- Tab-selection color animation now also applies to recent chats
- Removed 'Compare' menu autoselect when a compare chat conversation is selected
- Fixed UI consistency in Compare to match New Chat
- Removed sidebar animation and tab line, replaced with rounded selection for consistency
- Further adjustments to sidebar UI
- Further adjustments to compare chat UI

* Fixed sidebar collapse/expand for recent chats and recent runs not being clickable

* Chatbox, scroll, and menu fixes

- Fixed chatbox auto-expand height for multi-line text on the compare page
- Fixed chatbox UI to be consistent across compare and new chat
- Fixed scrolling being enabled on pages with no content, which also triggered the scroll-to-bottom button
- Fixed scroll-to-bottom button to only appear after scrolling up a reasonable amount instead of instantly
- Added shutdown studio button to the menu for easier access
- Fixed pop-up menu width to match the user button width

* Sidebar, fonts, and chat UI refinements

- Replaced logo PNG with real font text for 'unsloth' and 'BETA' label
- Added Hellix font and applied it across menus and UI elements
- Lighter scrollbar in the sidebar compared to other areas of the app
- Adjusted chat font and chat bubble styling
- Adjusted app menu design to stay consistent with the sidebar
- Adjusted text style for 'New Chat' and repositioned content/chatbox
- Adjusted model selector and top area UI
- Fixed footer text from 'LLM's' to 'LLMs'
- Fixed active selection border color incorrectly appearing on page refresh and during general navigation
- Logo now defaults to 'New Chat' when clicked

* Sidebar, model selector, and mobile UI fixes

- Further adjustments to sidebar UI and logo
- Changed right bar icon
- Model selector adjustments
- Collapsed sidebar now matches the content area background
- Adjusted Hellix font spacing across pages
- Fixed sidebar icon overlap on mobile screens

* Adjust sidebar icons

* Adjust sidebar icons

* Fixed compare chat UI and scrolling issues

* Fixed inference settings icon behavior and context info positioning

- Fixed top right inference settings icon to move into sidepanel during expand/collapse, matching left sidebar behavior
- Adjusted context information element positioning

* Fix: textarea overflow in system prompt editor

* Code block redesign, font, and chat bubble adjustments

- Redesigned code block colors and theme
- Changed code block font to Fira Code
- Fixed scrollbar disappearing when expanding/collapsing tool calls in chats
- Adjusted chat bubble background color

* Fix chat bubble background color in dark theme

* fix: restore textarea auto-sizing and scope prompt editor sizing

* fix: add explicit textarea field sizing for prompt editor overflow

* fix: generate chat nonce on click instead of render

* fix: respect training lock on logo navigation

* Refactor compare page dual chat scrolling behavior

* Revert "Refactor compare page dual chat scrolling behavior"

This reverts commit d056ec09f2.

---------

Co-authored-by: sneakr <hauzin@hotmail.com>
Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
2026-04-21 02:20:45 +04:00

63 lines
2 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { type RefObject, useCallback, useEffect, useRef } from "react";
/**
* Locks the nearest scrollable ancestor's scrollTop for the duration of a
* collapsible animation so the page doesn't jump when content height changes.
*
* Unlike @assistant-ui/react's `useScrollLock`, this hook does NOT toggle
* `scrollbar-width: none` on the container. Hiding the scrollbar mid-animation
* caused a visible disappear/reappear flicker on tool-call collapsibles.
*/
export function useCollapseScrollLock(
animatedElementRef: RefObject<HTMLElement | null>,
animationDurationMs: number,
): () => void {
const cleanupRef = useRef<(() => void) | null>(null);
useEffect(() => {
return () => {
cleanupRef.current?.();
};
}, []);
return useCallback(() => {
cleanupRef.current?.();
const animatedElement = animatedElementRef.current;
if (!animatedElement) return;
let scrollContainer: HTMLElement | null = animatedElement;
while (scrollContainer) {
const { overflowY } = getComputedStyle(scrollContainer);
if (overflowY === "scroll" || overflowY === "auto") {
break;
}
scrollContainer = scrollContainer.parentElement;
}
if (!scrollContainer) return;
const container = scrollContainer;
const scrollPosition = container.scrollTop;
const resetPosition = () => {
container.scrollTop = scrollPosition;
};
container.addEventListener("scroll", resetPosition);
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const cleanup = () => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
container.removeEventListener("scroll", resetPosition);
if (cleanupRef.current === cleanup) {
cleanupRef.current = null;
}
};
timeoutId = setTimeout(cleanup, animationDurationMs);
cleanupRef.current = cleanup;
}, [animatedElementRef, animationDurationMs]);
}