mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-24 08:36:33 +01:00
🔄 refactor: Migrate to react-resizable-panels v4 with Artifacts Header polish (#12356)
* 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:
parent
733a9364c0
commit
676641f3da
14 changed files with 92 additions and 118 deletions
|
|
@ -95,7 +95,7 @@
|
|||
"react-hook-form": "^7.43.9",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-resizable-panels": "^4.7.4",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"react-speech-recognition": "^3.10.0",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -189,6 +189,14 @@ export const sharedFiles = {
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
|
||||
<style>
|
||||
::-webkit-scrollbar{height:.1em;width:.5rem}
|
||||
::-webkit-scrollbar-thumb{background-color:rgba(0,0,0,.1);border-radius:9999px}
|
||||
::-webkit-scrollbar-track{background-color:transparent;border-radius:9999px}
|
||||
@media(prefers-color-scheme:dark){::-webkit-scrollbar-thumb{background-color:hsla(0,0%,100%,.1)}}
|
||||
*{scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.1) transparent}
|
||||
@media(prefers-color-scheme:dark){*{scrollbar-color:hsla(0,0%,100%,.1) transparent}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -113,24 +113,6 @@ export const extractContent = (
|
|||
return '';
|
||||
};
|
||||
|
||||
export const normalizeLayout = (layout: number[]) => {
|
||||
const sum = layout.reduce((acc, size) => acc + size, 0);
|
||||
if (Math.abs(sum - 100) < 0.01) {
|
||||
return layout.map((size) => Number(size.toFixed(2)));
|
||||
}
|
||||
|
||||
const factor = 100 / sum;
|
||||
const normalizedLayout = layout.map((size) => Number((size * factor).toFixed(2)));
|
||||
|
||||
const adjustedSum = normalizedLayout.reduce(
|
||||
(acc, size, index) => (index === layout.length - 1 ? acc : acc + size),
|
||||
0,
|
||||
);
|
||||
normalizedLayout[normalizedLayout.length - 1] = Number((100 - adjustedSum).toFixed(2));
|
||||
|
||||
return normalizedLayout;
|
||||
};
|
||||
|
||||
export const handleUIAction = async (result: UIActionResult, ask: TAskFunction) => {
|
||||
const supportedTypes = ['intent', 'tool', 'prompt'];
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,18 @@ const markdownCSS = `
|
|||
background-color: #21262d;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { height: 0.1em; width: 0.5rem; }
|
||||
::-webkit-scrollbar-thumb { background-color: rgba(0,0,0,0.1); border-radius: 9999px; }
|
||||
::-webkit-scrollbar-track { background-color: transparent; border-radius: 9999px; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
::-webkit-scrollbar-thumb { background-color: hsla(0,0%,100%,0.1); }
|
||||
}
|
||||
* { scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.1) transparent; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
* { scrollbar-color: hsla(0,0%,100%,0.1) transparent; }
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
|
|
|
|||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -461,7 +461,7 @@
|
|||
"react-hook-form": "^7.43.9",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-resizable-panels": "^4.7.4",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"react-speech-recognition": "^3.10.0",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
|
|
@ -38086,13 +38086,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-resizable-panels": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.6.tgz",
|
||||
"integrity": "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==",
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.7.4.tgz",
|
||||
"integrity": "sha512-1sehMbUJxZFj4imu1TuH+RS7Xe5Jo+3HyxBYafCEdxPmDeLAQNmKEByyxCQRl7xCLa5cLXSJ9T8acQyyiPsNdQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
|
|
@ -44131,7 +44131,7 @@
|
|||
"react-dom": "^18.2.0 || ^19.1.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-i18next": "^15.4.0 || ^15.6.0",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-resizable-panels": "^4.7.4",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
"tailwind-merge": "^1.9.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
"react-dom": "^18.2.0 || ^19.1.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-i18next": "^15.4.0 || ^15.6.0",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-resizable-panels": "^4.7.4",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
"tailwind-merge": "^1.9.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ interface RadioProps {
|
|||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
fullWidth?: boolean;
|
||||
'aria-labelledby'?: string;
|
||||
}
|
||||
|
|
@ -23,6 +24,7 @@ const Radio = memo(function Radio({
|
|||
onChange,
|
||||
disabled = false,
|
||||
className = '',
|
||||
buttonClassName = '',
|
||||
fullWidth = false,
|
||||
'aria-labelledby': ariaLabelledBy,
|
||||
}: RadioProps) {
|
||||
|
|
@ -45,7 +47,7 @@ const Radio = memo(function Radio({
|
|||
if (selectedButton && container) {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const buttonRect = selectedButton.getBoundingClientRect();
|
||||
const offsetLeft = buttonRect.left - containerRect.left - 4;
|
||||
const offsetLeft = buttonRect.left - containerRect.left;
|
||||
setBackgroundStyle({
|
||||
width: `${buttonRect.width}px`,
|
||||
transform: `translateX(${offsetLeft}px)`,
|
||||
|
|
@ -94,13 +96,13 @@ const Radio = memo(function Radio({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`relative ${fullWidth ? 'flex' : 'inline-flex'} items-center rounded-lg bg-muted p-1 ${className}`}
|
||||
className={`relative ${fullWidth ? 'flex' : 'inline-flex'} items-center rounded-lg bg-muted ${className}`}
|
||||
role="radiogroup"
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
>
|
||||
{selectedIndex >= 0 && isMounted && (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-1 rounded-md border border-border/50 bg-background shadow-sm transition-all duration-300 ease-out"
|
||||
className="pointer-events-none absolute inset-y-0 rounded-md border border-border/50 bg-background shadow-sm transition-all duration-300 ease-out"
|
||||
style={backgroundStyle}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -117,7 +119,7 @@ const Radio = memo(function Radio({
|
|||
disabled={disabled}
|
||||
className={`relative z-10 flex h-[34px] items-center justify-center gap-2 rounded-md px-4 text-sm font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
||||
currentValue === option.value ? 'text-foreground' : 'text-foreground'
|
||||
} ${disabled ? 'cursor-not-allowed opacity-50' : ''} ${fullWidth ? 'flex-1' : ''}`}
|
||||
} ${disabled ? 'cursor-not-allowed opacity-50' : ''} ${fullWidth ? 'flex-1' : ''} ${buttonClassName}`}
|
||||
>
|
||||
{option.icon && (
|
||||
<span className="flex-shrink-0" aria-hidden="true">
|
||||
|
|
|
|||
|
|
@ -1,30 +1,24 @@
|
|||
import { GripVertical } from 'lucide-react';
|
||||
import * as ResizablePrimitive from 'react-resizable-panels';
|
||||
|
||||
import { Group, Panel, Separator } from 'react-resizable-panels';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className = '',
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
className={cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', className)}
|
||||
{...props}
|
||||
/>
|
||||
const ResizablePanelGroup = ({ className = '', ...props }: ComponentProps<typeof Group>) => (
|
||||
<Group className={cn('h-full w-full', className)} {...props} />
|
||||
);
|
||||
|
||||
const ResizablePanel = ResizablePrimitive.Panel;
|
||||
const ResizablePanel = Panel;
|
||||
|
||||
const ResizableHandle = ({
|
||||
withHandle,
|
||||
className = '',
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
}: ComponentProps<typeof Separator> & {
|
||||
withHandle?: boolean;
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
<Separator
|
||||
className={cn(
|
||||
'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
|
||||
'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -34,29 +28,29 @@ const ResizableHandle = ({
|
|||
<GripVertical className="h-2.5 w-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
</Separator>
|
||||
);
|
||||
|
||||
const ResizableHandleAlt = ({
|
||||
withHandle,
|
||||
className = '',
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
}: ComponentProps<typeof Separator> & {
|
||||
withHandle?: boolean;
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
<Separator
|
||||
className={cn(
|
||||
'group relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
|
||||
'group relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="invisible z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border group-hover:visible group-active:visible group-data-[resize-handle-active]:visible">
|
||||
<div className="invisible z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border group-hover:visible group-active:visible group-data-[separator=active]:visible">
|
||||
<GripVertical className="h-2.5 w-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
</Separator>
|
||||
);
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle, ResizableHandleAlt };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue