feat: Enhance Artifacts and SidePanel components with improved mobile responsiveness and layout transitions

This commit is contained in:
Marco Beretta 2025-10-18 01:35:52 +02:00
parent 29b8314870
commit 44fa479bd4
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
2 changed files with 79 additions and 57 deletions

View file

@ -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>

View file

@ -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' : ''}`}