🔄 refactor: Migrate to react-resizable-panels v4 with Artifacts Header polish (#12356)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run

* chore: Update react-resizable-panels dependency to version 4.7.4

- Upgraded the "react-resizable-panels" package in package-lock.json, package.json, and client package.json files to ensure compatibility with the latest features and improvements.
- Adjusted peer dependencies for React and ReactDOM to align with the new version requirements.

* refactor: Update Share and SidePanel components to `react-resizable-panels` v4

- Refactored the ShareArtifactsContainer to utilize a new layout change handler, enhancing artifact panel resizing functionality.
- Updated ArtifactsPanel to use the new `usePanelRef` hook, improving panel reference management.
- Simplified SidePanelGroup by removing unnecessary layout normalization and integrating default layout handling with localStorage.
- Removed the deprecated `normalizeLayout` utility function to streamline the codebase.
- Adjusted Resizable components to ensure consistent sizing and layout behavior across panels.

* style: Enhance scrollbar appearance across application

- Added custom scrollbar styles to both artifacts and markdown files, improving aesthetics and user experience.
- Implemented dark mode adjustments for scrollbar visibility, ensuring consistency across different color schemes.

* style: Standardize button sizes and layout in Artifacts components

- Updated button dimensions to a consistent height of 9 units across various components including Artifacts, Code, and DownloadArtifact.
- Adjusted padding and layout properties in the Artifacts header for improved visual consistency.
- Enhanced the Radio component to accept a new `buttonClassName` prop for better customization of button styles.

* chore: import order
This commit is contained in:
Danny Avila 2026-03-22 02:21:27 -04:00 committed by GitHub
parent 733a9364c0
commit 676641f3da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 92 additions and 118 deletions

View file

@ -216,7 +216,7 @@ export default function Artifacts() {
{/* Header */}
<div
className={cn(
'flex flex-shrink-0 items-center justify-between gap-2 border-b border-border-light bg-surface-primary-alt px-3 py-2 transition-all duration-300',
'flex h-[52px] flex-shrink-0 items-center justify-between gap-2 border-b border-border-light bg-surface-primary-alt p-2 transition-all duration-300',
isMobile ? 'justify-center' : 'overflow-hidden',
)}
>
@ -234,6 +234,7 @@ export default function Artifacts() {
value={activeTab}
onChange={setActiveTab}
disabled={isMutating && activeTab !== 'code'}
buttonClassName="h-9 px-3 gap-1.5"
/>
</div>
)}
@ -249,6 +250,7 @@ export default function Artifacts() {
<Button
size="icon"
variant="ghost"
className="h-9 w-9"
onClick={handleRefresh}
disabled={isRefreshing}
aria-label={localize('com_ui_refresh')}
@ -284,6 +286,7 @@ export default function Artifacts() {
<Button
size="icon"
variant="ghost"
className="h-9 w-9"
onClick={closeArtifacts}
aria-label={localize('com_ui_close')}
>

View file

@ -40,6 +40,7 @@ export const CopyCodeButton: React.FC<{ content: string }> = ({ content }) => {
<Button
size="icon"
variant="ghost"
className="h-9 w-9"
onClick={handleCopy}
aria-label={isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
>

View file

@ -38,6 +38,7 @@ const DownloadArtifact = ({ artifact }: { artifact: Artifact }) => {
<Button
size="icon"
variant="ghost"
className="h-9 w-9"
onClick={handleDownload}
aria-label={localize('com_ui_download_artifact')}
>

View file

@ -83,14 +83,9 @@ export function ShareArtifactsContainer({
const normalizedArtifactSize = Math.min(60, Math.max(20, artifactPanelSize));
/**
* Handles artifact panel resize and persists size to localStorage
*/
const handleLayoutChange = (sizes: number[]) => {
if (sizes.length < 2) {
return;
}
const newSize = sizes[1];
const handleLayoutChanged = (layout: Record<string, number | string>) => {
const raw = layout['share-artifacts'];
const newSize = typeof raw === 'string' ? parseFloat(raw) : raw;
if (!Number.isFinite(newSize)) {
return;
}
@ -115,24 +110,22 @@ export function ShareArtifactsContainer({
return (
<ResizablePanelGroup
direction="horizontal"
className="flex h-full w-full"
onLayout={handleLayoutChange}
orientation="horizontal"
className="h-full w-full"
onLayoutChanged={handleLayoutChanged}
>
<ResizablePanel
defaultSize={100 - normalizedArtifactSize}
minSize={35}
order={1}
defaultSize={`${100 - normalizedArtifactSize}`}
minSize="35"
id="share-content"
>
{mainContent}
</ResizablePanel>
<ResizableHandleAlt withHandle className="bg-border-medium text-text-primary" />
<ResizablePanel
defaultSize={normalizedArtifactSize}
minSize={20}
maxSize={60}
order={2}
defaultSize={`${normalizedArtifactSize}`}
minSize="20"
maxSize="60"
id="share-artifacts"
>
<ShareArtifactsPanel contextValue={artifactsContextValue} />

View file

@ -1,27 +1,21 @@
import { useRef, useEffect, memo } from 'react';
import { useEffect, memo } from 'react';
import { usePanelRef } from 'react-resizable-panels';
import { ResizableHandleAlt, ResizablePanel } from '@librechat/client';
import type { ImperativePanelHandle } from 'react-resizable-panels';
interface ArtifactsPanelProps {
artifacts: React.ReactNode | null;
currentLayout: number[];
minSizeMain: number;
minSizeMain: string;
shouldRender: boolean;
onRenderChange: (shouldRender: boolean) => void;
}
/**
* ArtifactsPanel component - memoized to prevent unnecessary re-renders
* Only re-renders when artifacts visibility or layout changes
*/
const ArtifactsPanel = memo(function ArtifactsPanel({
artifacts,
currentLayout,
minSizeMain,
shouldRender,
onRenderChange,
}: ArtifactsPanelProps) {
const artifactsPanelRef = useRef<ImperativePanelHandle>(null);
const artifactsPanelRef = usePanelRef();
useEffect(() => {
if (artifacts != null) {
@ -34,7 +28,7 @@ const ArtifactsPanel = memo(function ArtifactsPanel({
} else if (shouldRender) {
onRenderChange(false);
}
}, [artifacts, shouldRender, onRenderChange]);
}, [artifacts, shouldRender, onRenderChange, artifactsPanelRef]);
if (!shouldRender) {
return null;
@ -46,13 +40,12 @@ const ArtifactsPanel = memo(function ArtifactsPanel({
<ResizableHandleAlt withHandle className="bg-border-medium text-text-primary" />
)}
<ResizablePanel
ref={artifactsPanelRef}
defaultSize={artifacts != null ? currentLayout[1] : 0}
minSize={minSizeMain}
maxSize={70}
defaultSize="50"
maxSize="70"
collapsedSize="0"
collapsible={true}
collapsedSize={0}
order={2}
minSize={minSizeMain}
panelRef={artifactsPanelRef}
id="artifacts-panel"
>
<div className="h-full min-w-[400px] overflow-hidden">{artifacts}</div>

View file

@ -1,8 +1,10 @@
import { useState, useEffect, useMemo, memo } from 'react';
import throttle from 'lodash/throttle';
import { useState, memo } from 'react';
import { useDefaultLayout } from 'react-resizable-panels';
import { ResizablePanel, ResizablePanelGroup, useMediaQuery } from '@librechat/client';
import ArtifactsPanel from './ArtifactsPanel';
import { normalizeLayout } from '~/utils';
const PANEL_IDS_SINGLE = ['messages-view'];
const PANEL_IDS_SPLIT = ['messages-view', 'artifacts-panel'];
interface SidePanelProps {
artifacts?: React.ReactNode;
@ -13,46 +15,29 @@ const SidePanelGroup = memo(({ artifacts, children }: SidePanelProps) => {
const [shouldRenderArtifacts, setShouldRenderArtifacts] = useState(artifacts != null);
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const currentLayout = useMemo(() => {
if (artifacts == null) {
return [100];
}
return normalizeLayout([50, 50]);
}, [artifacts]);
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
id: 'side-panel-layout',
panelIds: artifacts != null ? PANEL_IDS_SPLIT : PANEL_IDS_SINGLE,
storage: localStorage,
});
const throttledSaveLayout = useMemo(
() =>
throttle((sizes: number[]) => {
const normalizedSizes = normalizeLayout(sizes);
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
}, 350),
[],
);
useEffect(() => () => throttledSaveLayout.cancel(), [throttledSaveLayout]);
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
const minSizeMain = artifacts != null ? '15' : '30';
return (
<>
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes) => throttledSaveLayout(sizes)}
className="relative h-full w-full flex-1 overflow-auto bg-presentation"
orientation="horizontal"
defaultLayout={defaultLayout}
onLayoutChanged={onLayoutChanged}
className="relative flex-1 bg-presentation"
>
<ResizablePanel
defaultSize={currentLayout[0]}
minSize={minSizeMain}
order={1}
id="messages-view"
>
<ResizablePanel defaultSize="50" minSize={minSizeMain} id="messages-view">
{children}
</ResizablePanel>
{!isSmallScreen && (
<ArtifactsPanel
artifacts={artifacts}
currentLayout={currentLayout}
minSizeMain={minSizeMain}
shouldRender={shouldRenderArtifacts}
onRenderChange={setShouldRenderArtifacts}