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() {
|
export default function Artifacts() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { isMutating } = useEditorContext();
|
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 editorRef = useRef<CodeEditorRef>();
|
||||||
const previewRef = useRef<SandpackPreviewRef>();
|
const previewRef = useRef<SandpackPreviewRef>();
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
@ -52,76 +52,95 @@ export default function Artifacts() {
|
||||||
|
|
||||||
const closeArtifacts = () => {
|
const closeArtifacts = () => {
|
||||||
setIsVisible(false);
|
setIsVisible(false);
|
||||||
setTimeout(() => setArtifactsVisible(false), 300);
|
setTimeout(() => setArtifactsVisible(false), isMobile ? 400 : 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Root value={activeTab} onValueChange={setActiveTab} asChild>
|
<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
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex flex-col overflow-hidden bg-surface-primary text-xl text-text-primary shadow-xl transition-all duration-500 ease-in-out`,
|
'flex h-full w-full flex-col overflow-hidden bg-surface-primary text-xl text-text-primary',
|
||||||
isVisible ? 'opacity-100 blur-0' : 'opacity-0 blur-sm',
|
isMobile
|
||||||
isMobile ? 'fixed inset-x-0 bottom-0 z-[100] h-[90vh] rounded-t-xl' : 'h-full w-full',
|
? 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
|
<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 && (
|
{!isMobile && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Tabs.List className="relative inline-flex h-9 gap-2 rounded-xl bg-surface-tertiary p-0.5">
|
<Tabs.List className="relative inline-flex h-9 gap-2 rounded-xl bg-surface-tertiary p-0.5">
|
||||||
<div
|
<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'
|
activeTab === 'code'
|
||||||
? 'w-[42%] translate-x-0'
|
? 'w-[42%] translate-x-0'
|
||||||
: 'w-[50%] translate-x-[calc(100%-0.250rem)]'
|
: 'w-[50%] translate-x-[calc(100%-0.250rem)]',
|
||||||
}`}
|
)}
|
||||||
/>
|
/>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="code"
|
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" />
|
<Code className="size-3" />
|
||||||
<span className="transition-all duration-200 ease-out">
|
<span>{localize('com_ui_code')}</span>
|
||||||
{localize('com_ui_code')}
|
|
||||||
</span>
|
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="preview"
|
value="preview"
|
||||||
disabled={isMutating}
|
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" />
|
<Play className="size-3" />
|
||||||
<span className="transition-all duration-200 ease-out">
|
<span>{localize('com_ui_preview')}</span>
|
||||||
{localize('com_ui_preview')}
|
|
||||||
</span>
|
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex min-w-max items-center gap-3">
|
<div className="flex min-w-max items-center gap-2">
|
||||||
{activeTab === 'preview' && (
|
{activeTab === 'preview' && (
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
disabled={isRefreshing}
|
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 ? (
|
{isRefreshing ? <Spinner size={16} /> : <RefreshCw size={16} />}
|
||||||
<Spinner size={16} />
|
|
||||||
) : (
|
|
||||||
<RefreshCw
|
|
||||||
size={16}
|
|
||||||
className={cn('transform', isRefreshing ? 'animate-spin' : '')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{activeTab !== 'preview' && isMutating && (
|
{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 && (
|
{orderedArtifactIds.length > 1 && (
|
||||||
<ArtifactVersion
|
<ArtifactVersion
|
||||||
|
|
@ -129,7 +148,9 @@ export default function Artifacts() {
|
||||||
totalVersions={orderedArtifactIds.length}
|
totalVersions={orderedArtifactIds.length}
|
||||||
onVersionChange={(index) => {
|
onVersionChange={(index) => {
|
||||||
const target = orderedArtifactIds[index];
|
const target = orderedArtifactIds[index];
|
||||||
if (target) setCurrentArtifactId(target);
|
if (target) {
|
||||||
|
setCurrentArtifactId(target);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -139,54 +160,47 @@ export default function Artifacts() {
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={closeArtifacts}
|
onClick={closeArtifacts}
|
||||||
disabled={isRefreshing}
|
aria-label={localize('com_ui_close')}
|
||||||
aria-label="Close Artifacts"
|
className="h-8 w-8 transition-transform duration-150 ease-out hover:scale-105 active:scale-95"
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<ArtifactTabs
|
||||||
isMermaid={isMermaid}
|
|
||||||
artifact={currentArtifact}
|
artifact={currentArtifact}
|
||||||
editorRef={editorRef as React.MutableRefObject<CodeEditorRef>}
|
editorRef={editorRef as React.MutableRefObject<CodeEditorRef>}
|
||||||
previewRef={previewRef as React.MutableRefObject<SandpackPreviewRef>}
|
previewRef={previewRef as React.MutableRefObject<SandpackPreviewRef>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Tab Switcher */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="flex w-full items-center justify-center bg-surface-primary-alt px-3 pb-2 pt-2">
|
<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-9 w-full rounded-xl bg-surface-tertiary px-1 py-0.5">
|
<Tabs.List className="relative flex h-10 w-full rounded-xl bg-surface-tertiary p-1">
|
||||||
{/* sliding background: exactly half-width, moves 0% or 100% */}
|
|
||||||
<div
|
<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
|
<Tabs.Trigger
|
||||||
value="code"
|
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.5" />
|
||||||
<Code className="size-3" />
|
<span>{localize('com_ui_code')}</span>
|
||||||
<span className="transition-all duration-200 ease-out">
|
|
||||||
{localize('com_ui_code')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
|
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="preview"
|
value="preview"
|
||||||
disabled={isMutating}
|
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.5" />
|
||||||
<Play className="size-3" />
|
<span>{localize('com_ui_preview')}</span>
|
||||||
<span className="transition-all duration-200 ease-out">
|
|
||||||
{localize('com_ui_preview')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -109,24 +109,29 @@ const SidePanelGroup = memo(
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
onLayout={(sizes) => throttledSaveLayout(sizes)}
|
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
|
<ResizablePanel
|
||||||
defaultSize={currentLayout[0]}
|
defaultSize={currentLayout[0]}
|
||||||
minSize={minSizeMain}
|
minSize={minSizeMain}
|
||||||
order={1}
|
order={1}
|
||||||
id="messages-view"
|
id="messages-view"
|
||||||
|
className="ease-[cubic-bezier(0.25,0.46,0.45,0.94)] transition-all duration-500"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ResizablePanel>
|
</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
|
<ResizablePanel
|
||||||
defaultSize={currentLayout[1]}
|
defaultSize={currentLayout[1]}
|
||||||
minSize={minSizeMain}
|
minSize={minSizeMain}
|
||||||
order={2}
|
order={2}
|
||||||
id="artifacts-panel"
|
id="artifacts-panel"
|
||||||
|
className="ease-[cubic-bezier(0.25,0.46,0.45,0.94)] transition-all duration-500"
|
||||||
>
|
>
|
||||||
{artifacts}
|
{artifacts}
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|
@ -149,6 +154,9 @@ const SidePanelGroup = memo(
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
|
{artifacts != null && isSmallScreen && (
|
||||||
|
<div className="fixed inset-0 z-[100]">{artifacts}</div>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
aria-label="Close right side panel"
|
aria-label="Close right side panel"
|
||||||
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue