feat: Enhance scrolling behavior and styles across components; add centralized scroll utilities

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-28 22:51:50 +10:00
parent e6af56a417
commit cad4852878
8 changed files with 151 additions and 61 deletions

View File

@@ -172,6 +172,32 @@ export type ScrollInfo = {
viewHeight: number;
scrollable: boolean;
};
export type ScrollBehavior = 'auto' | 'instant' | 'smooth';
export type ScrollPosition = {
top?: number;
left?: number;
behavior?: ScrollBehavior;
};
/**
* Centralized wrapper for element scrolling.
*/
export const scrollToPosition = (scrollEl: HTMLElement, position: ScrollPosition): void => {
scrollEl.scrollTo(position);
};
/**
* Centralized wrapper for scrolling a target into the current viewport.
*/
export const scrollElementIntoView = (
element: HTMLElement,
options?: ScrollIntoViewOptions
): void => {
element.scrollIntoView(options);
};
export const getScrollInfo = (target: HTMLElement): ScrollInfo => ({
offsetTop: Math.round(target.offsetTop),
top: Math.round(target.scrollTop),
@@ -180,17 +206,44 @@ export const getScrollInfo = (target: HTMLElement): ScrollInfo => ({
scrollable: target.scrollHeight > target.offsetHeight,
});
export const scrollToBottom = (scrollEl: HTMLElement, behavior?: 'auto' | 'instant' | 'smooth') => {
scrollEl.scrollTo({
export const scrollToBottom = (scrollEl: HTMLElement, behavior?: ScrollBehavior) => {
scrollToPosition(scrollEl, {
top: Math.round(scrollEl.scrollHeight - scrollEl.offsetHeight),
behavior,
});
};
/**
* Check if scroll element is at or near the bottom (within threshold).
* Useful for "sticky to bottom" behavior in chat-like interfaces.
* @param scrollEl - The scrollable element
* @param threshold - Distance from bottom in pixels (default: 50px)
* @returns true if scrolled to bottom within threshold
*/
export const isScrolledToBottom = (scrollEl: HTMLElement, threshold = 50): boolean => {
const { scrollTop, offsetHeight, scrollHeight } = scrollEl;
const scrollPosition = scrollTop + offsetHeight;
return scrollHeight - scrollPosition <= threshold;
};
/**
* Shared auto-scroll behavior for chat-like timelines that should stay pinned to bottom.
* Only scrolls if already near the bottom unless a call site explicitly forces it.
*/
export const autoScrollToBottom = (
scrollEl: HTMLElement,
smooth = false,
force = false
): void => {
if (force || isScrolledToBottom(scrollEl)) {
scrollToBottom(scrollEl, smooth ? 'smooth' : 'instant');
}
};
export const copyToClipboard = (text: string) => {
// Try Electron clipboard first (if available)
if ('electron' in window && (window as any).electron?.clipboard?.writeText) {
(window as any).electron.clipboard.writeText(text);
if ('electron' in window && window.electron?.clipboard?.writeText) {
window.electron.clipboard.writeText(text);
return;
}