mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-22 19:30:15 +01:00
✨ feat: Enhance Artifacts and SidePanel components with improved mobile responsiveness and layout transitions
This commit is contained in:
parent
29b8314870
commit
44fa479bd4
2 changed files with 79 additions and 57 deletions
|
|
@ -17,7 +17,7 @@ import store from '~/store';
|
|||
export default function Artifacts() {
|
||||
const localize = useLocalize();
|
||||
const { isMutating } = useEditorContext();
|
||||
const isMobile = useMediaQuery('(max-width: 868px)'); // DO NOT change this value, it is used to determine the layout of the artifacts panel ONLY
|
||||
const isMobile = useMediaQuery('(max-width: 868px)');
|
||||
const editorRef = useRef<CodeEditorRef>();
|
||||
const previewRef = useRef<SandpackPreviewRef>();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
|
@ -52,76 +52,95 @@ export default function Artifacts() {
|
|||
|
||||
const closeArtifacts = () => {
|
||||
setIsVisible(false);
|
||||
setTimeout(() => setArtifactsVisible(false), 300);
|
||||
setTimeout(() => setArtifactsVisible(false), isMobile ? 400 : 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs.Root value={activeTab} onValueChange={setActiveTab} asChild>
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{/* Mobile backdrop */}
|
||||
{isMobile && (
|
||||
<div
|
||||
className={cn(
|
||||
'duration-400 ease-[cubic-bezier(0.25,0.46,0.45,0.94)] fixed inset-0 z-[99] bg-black/40 backdrop-blur-md transition-opacity',
|
||||
isVisible ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
onClick={closeArtifacts}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
`flex flex-col overflow-hidden bg-surface-primary text-xl text-text-primary shadow-xl transition-all duration-500 ease-in-out`,
|
||||
isVisible ? 'opacity-100 blur-0' : 'opacity-0 blur-sm',
|
||||
isMobile ? 'fixed inset-x-0 bottom-0 z-[100] h-[90vh] rounded-t-xl' : 'h-full w-full',
|
||||
'flex h-full w-full flex-col overflow-hidden bg-surface-primary text-xl text-text-primary',
|
||||
isMobile
|
||||
? cn(
|
||||
'duration-400 ease-[cubic-bezier(0.25,0.46,0.45,0.94)] fixed inset-x-0 bottom-0 z-[100] h-[90vh] rounded-t-[20px] shadow-[0_-10px_40px_rgba(0,0,0,0.3)] transition-transform will-change-transform',
|
||||
isVisible ? 'translate-y-0' : 'translate-y-full',
|
||||
)
|
||||
: cn(
|
||||
'ease-[cubic-bezier(0.25,0.46,0.45,0.94)] shadow-xl transition-all duration-500 will-change-transform',
|
||||
isVisible ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0',
|
||||
),
|
||||
)}
|
||||
>
|
||||
{/* Mobile drag indicator */}
|
||||
{isMobile && (
|
||||
<div className="flex flex-shrink-0 items-center justify-center pb-1.5 pt-2">
|
||||
<div className="h-1 w-10 rounded-full bg-border-medium opacity-50" />
|
||||
</div>
|
||||
)}
|
||||
{/* Header */}
|
||||
<div
|
||||
className={`flex items-center ${isMobile ? 'justify-center' : 'justify-between'} overflow-x-auto bg-surface-primary-alt ${activeTab === 'code' ? 'p-3' : 'p-2'}`}
|
||||
className={cn(
|
||||
'flex flex-shrink-0 items-center justify-between gap-2 overflow-x-auto border-b border-border-light bg-surface-primary-alt px-3 py-2',
|
||||
isMobile && 'justify-center',
|
||||
)}
|
||||
>
|
||||
{!isMobile && (
|
||||
<div className="flex items-center">
|
||||
<Tabs.List className="relative inline-flex h-9 gap-2 rounded-xl bg-surface-tertiary p-0.5">
|
||||
<div
|
||||
className={`absolute top-0.5 h-8 rounded-xl bg-surface-primary-alt transition-transform duration-200 ease-out ${
|
||||
className={cn(
|
||||
'absolute top-0.5 h-8 rounded-xl bg-surface-primary-alt transition-transform duration-200 ease-out',
|
||||
activeTab === 'code'
|
||||
? 'w-[42%] translate-x-0'
|
||||
: 'w-[50%] translate-x-[calc(100%-0.250rem)]'
|
||||
}`}
|
||||
: 'w-[50%] translate-x-[calc(100%-0.250rem)]',
|
||||
)}
|
||||
/>
|
||||
<Tabs.Trigger
|
||||
value="code"
|
||||
className="relative z-10 flex items-center gap-1.5 rounded-xl border-transparent py-1 pl-2.5 pr-2.5 text-xs font-medium transition-all duration-200 ease-out hover:text-text-primary data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
className="relative z-10 flex items-center gap-1.5 rounded-xl border-transparent px-3 py-1 text-xs font-medium transition-all duration-200 ease-out hover:text-text-primary data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
>
|
||||
<Code className="size-3" />
|
||||
<span className="transition-all duration-200 ease-out">
|
||||
{localize('com_ui_code')}
|
||||
</span>
|
||||
<span>{localize('com_ui_code')}</span>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="preview"
|
||||
disabled={isMutating}
|
||||
className="relative z-10 flex items-center gap-2 rounded-xl border-transparent py-1 pl-2.5 pr-2.5 text-xs font-medium transition-all duration-200 ease-out hover:text-text-primary disabled:cursor-not-allowed disabled:opacity-50 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
className="relative z-10 flex items-center gap-1.5 rounded-xl border-transparent px-3 py-1 text-xs font-medium transition-all duration-200 ease-out hover:text-text-primary disabled:cursor-not-allowed disabled:opacity-50 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
>
|
||||
<Play className="size-3" />
|
||||
<span className="transition-all duration-200 ease-out">
|
||||
{localize('com_ui_preview')}
|
||||
</span>
|
||||
<span>{localize('com_ui_preview')}</span>
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex min-w-max items-center gap-3">
|
||||
<div className="flex min-w-max items-center gap-2">
|
||||
{activeTab === 'preview' && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
aria-label="Refresh"
|
||||
aria-label={localize('com_ui_refresh')}
|
||||
className="h-8 w-8 transition-transform duration-150 ease-out hover:scale-105 active:scale-95"
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<Spinner size={16} />
|
||||
) : (
|
||||
<RefreshCw
|
||||
size={16}
|
||||
className={cn('transform', isRefreshing ? 'animate-spin' : '')}
|
||||
/>
|
||||
)}
|
||||
{isRefreshing ? <Spinner size={16} /> : <RefreshCw size={16} />}
|
||||
</Button>
|
||||
)}
|
||||
{activeTab !== 'preview' && isMutating && (
|
||||
<RefreshCw size={16} className="mr-2 animate-spin text-text-secondary" />
|
||||
<RefreshCw size={16} className="animate-spin text-text-secondary" />
|
||||
)}
|
||||
{orderedArtifactIds.length > 1 && (
|
||||
<ArtifactVersion
|
||||
|
|
@ -129,7 +148,9 @@ export default function Artifacts() {
|
|||
totalVersions={orderedArtifactIds.length}
|
||||
onVersionChange={(index) => {
|
||||
const target = orderedArtifactIds[index];
|
||||
if (target) setCurrentArtifactId(target);
|
||||
if (target) {
|
||||
setCurrentArtifactId(target);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -139,54 +160,47 @@ export default function Artifacts() {
|
|||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={closeArtifacts}
|
||||
disabled={isRefreshing}
|
||||
aria-label="Close Artifacts"
|
||||
aria-label={localize('com_ui_close')}
|
||||
className="h-8 w-8 transition-transform duration-150 ease-out hover:scale-105 active:scale-95"
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={isMobile ? 'flex-grow overflow-auto' : ''}>
|
||||
{/* Content Area - This is the key fix */}
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<ArtifactTabs
|
||||
isMermaid={isMermaid}
|
||||
artifact={currentArtifact}
|
||||
editorRef={editorRef as React.MutableRefObject<CodeEditorRef>}
|
||||
previewRef={previewRef as React.MutableRefObject<SandpackPreviewRef>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mobile Tab Switcher */}
|
||||
{isMobile && (
|
||||
<div className="flex w-full items-center justify-center bg-surface-primary-alt px-3 pb-2 pt-2">
|
||||
<Tabs.List className="relative flex h-9 w-full rounded-xl bg-surface-tertiary px-1 py-0.5">
|
||||
{/* sliding background: exactly half-width, moves 0% or 100% */}
|
||||
<div className="pb-safe-offset-3 flex-shrink-0 border-t border-border-light bg-surface-primary-alt px-3 pt-2">
|
||||
<Tabs.List className="relative flex h-10 w-full rounded-xl bg-surface-tertiary p-1">
|
||||
<div
|
||||
className={`absolute left-0 top-0.5 h-8 w-1/2 rounded-xl bg-surface-primary-alt transition-transform duration-200 ease-out ${activeTab === 'code' ? 'translate-x-0' : 'translate-x-full'} `}
|
||||
className={cn(
|
||||
'duration-[250ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] absolute left-1 top-1 h-8 w-[calc(50%-0.25rem)] rounded-lg bg-surface-primary-alt shadow-sm transition-transform',
|
||||
activeTab === 'code' ? 'translate-x-0' : 'translate-x-[calc(100%+0.5rem)]',
|
||||
)}
|
||||
/>
|
||||
|
||||
<Tabs.Trigger
|
||||
value="code"
|
||||
className="relative z-10 flex w-1/2 items-center justify-center rounded-xl border-transparent py-1 text-center text-xs font-medium transition-all duration-200 ease-out hover:text-text-primary data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
className="relative z-10 flex w-1/2 items-center justify-center gap-1.5 rounded-lg border-transparent py-1.5 text-xs font-medium transition-all duration-150 ease-out active:scale-95 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Code className="size-3" />
|
||||
<span className="transition-all duration-200 ease-out">
|
||||
{localize('com_ui_code')}
|
||||
</span>
|
||||
</div>
|
||||
<Code className="size-3.5" />
|
||||
<span>{localize('com_ui_code')}</span>
|
||||
</Tabs.Trigger>
|
||||
|
||||
<Tabs.Trigger
|
||||
value="preview"
|
||||
disabled={isMutating}
|
||||
className="relative z-10 flex w-1/2 items-center justify-center rounded-xl border-transparent py-1 text-center text-xs font-medium transition-all duration-200 ease-out hover:text-text-primary disabled:cursor-not-allowed disabled:opacity-50 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
className="relative z-10 flex w-1/2 items-center justify-center gap-1.5 rounded-lg border-transparent py-1.5 text-xs font-medium transition-all duration-150 ease-out active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Play className="size-3" />
|
||||
<span className="transition-all duration-200 ease-out">
|
||||
{localize('com_ui_preview')}
|
||||
</span>
|
||||
</div>
|
||||
<Play className="size-3.5" />
|
||||
<span>{localize('com_ui_preview')}</span>
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -109,24 +109,29 @@ const SidePanelGroup = memo(
|
|||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
onLayout={(sizes) => throttledSaveLayout(sizes)}
|
||||
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation"
|
||||
className="ease-[cubic-bezier(0.25,0.46,0.45,0.94)] relative h-full w-full flex-1 overflow-auto bg-presentation transition-all duration-500"
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={currentLayout[0]}
|
||||
minSize={minSizeMain}
|
||||
order={1}
|
||||
id="messages-view"
|
||||
className="ease-[cubic-bezier(0.25,0.46,0.45,0.94)] transition-all duration-500"
|
||||
>
|
||||
{children}
|
||||
</ResizablePanel>
|
||||
{artifacts != null && (
|
||||
{artifacts != null && !isSmallScreen && (
|
||||
<>
|
||||
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium text-text-primary" />
|
||||
<ResizableHandleAlt
|
||||
withHandle
|
||||
className="ml-3 bg-border-medium text-text-primary transition-opacity duration-300"
|
||||
/>
|
||||
<ResizablePanel
|
||||
defaultSize={currentLayout[1]}
|
||||
minSize={minSizeMain}
|
||||
order={2}
|
||||
id="artifacts-panel"
|
||||
className="ease-[cubic-bezier(0.25,0.46,0.45,0.94)] transition-all duration-500"
|
||||
>
|
||||
{artifacts}
|
||||
</ResizablePanel>
|
||||
|
|
@ -149,6 +154,9 @@ const SidePanelGroup = memo(
|
|||
/>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
{artifacts != null && isSmallScreen && (
|
||||
<div className="fixed inset-0 z-[100]">{artifacts}</div>
|
||||
)}
|
||||
<button
|
||||
aria-label="Close right side panel"
|
||||
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue