mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-22 19:30:15 +01:00
feat: Enhance artifact panel animations and improve UI responsiveness
- Updated Thinking component button styles for smoother transitions. - Implemented dynamic rendering for artifacts panel with animation effects. - Refactored localization keys for consistency across multiple languages. - Added new CSS animations for iOS-inspired smooth transitions. - Improved Tailwind CSS configuration to support enhanced animation effects.
This commit is contained in:
parent
44fa479bd4
commit
7e1d02bcc3
35 changed files with 416 additions and 104 deletions
|
|
@ -55,40 +55,52 @@ const ArtifactButton = ({ artifact }: { artifact: Artifact | null }) => {
|
|||
|
||||
return (
|
||||
<div className="group relative my-4 rounded-xl text-sm text-text-primary">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!location.pathname.includes('/c/')) {
|
||||
{(() => {
|
||||
const handleClick = () => {
|
||||
if (!location.pathname.includes('/c/')) return;
|
||||
|
||||
if (isSelected) {
|
||||
resetCurrentArtifactId();
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
resetCurrentArtifactId();
|
||||
setVisible(true);
|
||||
|
||||
if (artifacts?.[artifact.id] == null) {
|
||||
setArtifacts(visibleArtifacts);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setCurrentArtifactId(artifact.id);
|
||||
}, 15);
|
||||
}}
|
||||
className={
|
||||
`relative overflow-hidden rounded-xl transition-all duration-200 hover:border-border-medium hover:bg-surface-hover hover:shadow-lg ` +
|
||||
};
|
||||
|
||||
const buttonClass =
|
||||
`relative overflow-hidden rounded-xl transition-all duration-300 hover:border-border-medium hover:bg-surface-hover hover:shadow-lg active:scale-[0.98] ` +
|
||||
(isSelected
|
||||
? 'border-border-medium bg-surface-hover shadow-lg'
|
||||
: 'border-border-light bg-surface-tertiary shadow-sm')
|
||||
}
|
||||
>
|
||||
<div className="w-fit p-2">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<FilePreview fileType={fileType} className="relative" />
|
||||
<div className="overflow-hidden text-left">
|
||||
<div className="truncate font-medium">{artifact.title}</div>
|
||||
<div className="truncate text-text-secondary">
|
||||
{localize('com_ui_artifact_click')}
|
||||
: 'border-border-light bg-surface-tertiary shadow-sm');
|
||||
|
||||
const actionLabel = isSelected
|
||||
? localize('com_ui_click_to_close')
|
||||
: localize('com_ui_click_to_open');
|
||||
|
||||
return (
|
||||
<button type="button" onClick={handleClick} className={buttonClass}>
|
||||
<div className="w-fit p-2">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<FilePreview fileType={fileType} className="relative" />
|
||||
<div className="overflow-hidden text-left">
|
||||
<div className="truncate font-medium">{artifact.title}</div>
|
||||
<div className="truncate text-text-secondary">{actionLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
);
|
||||
})()}
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import React, { memo, useMemo, type MutableRefObject } from 'react';
|
||||
import { SandpackPreview, SandpackProvider } from '@codesandbox/sandpack-react/unstyled';
|
||||
import type { SandpackProviderProps, SandpackPreviewRef, PreviewProps } from '@codesandbox/sandpack-react/unstyled';
|
||||
import type {
|
||||
SandpackProviderProps,
|
||||
SandpackPreviewRef,
|
||||
PreviewProps,
|
||||
} from '@codesandbox/sandpack-react/unstyled';
|
||||
import type { TStartupConfig } from 'librechat-data-provider';
|
||||
import type { ArtifactFiles } from '~/common';
|
||||
import { sharedFiles, sharedOptions } from '~/utils/artifacts';
|
||||
|
|
|
|||
|
|
@ -39,12 +39,16 @@ export default function ArtifactTabs({
|
|||
const { files, fileKey, template, sharedProps } = useArtifactProps({ artifact });
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<Tabs.Content
|
||||
ref={contentRef}
|
||||
value="code"
|
||||
id="artifacts-code"
|
||||
className={cn('flex-grow overflow-auto')}
|
||||
className={cn(
|
||||
'h-full w-full flex-grow overflow-auto',
|
||||
'data-[state=active]:duration-200 data-[state=active]:animate-in data-[state=active]:fade-in-0',
|
||||
'data-[state=inactive]:duration-150 data-[state=inactive]:animate-out data-[state=inactive]:fade-out-0',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ArtifactCodeEditor
|
||||
|
|
@ -56,7 +60,16 @@ export default function ArtifactTabs({
|
|||
sharedProps={sharedProps}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="preview" className="flex-grow overflow-auto" tabIndex={-1}>
|
||||
|
||||
<Tabs.Content
|
||||
value="preview"
|
||||
className={cn(
|
||||
'h-full w-full flex-grow overflow-auto',
|
||||
'data-[state=active]:duration-200 data-[state=active]:animate-in data-[state=active]:fade-in-0',
|
||||
'data-[state=inactive]:duration-150 data-[state=inactive]:animate-out data-[state=inactive]:fade-out-0',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ArtifactPreview
|
||||
files={files}
|
||||
fileKey={fileKey}
|
||||
|
|
@ -67,6 +80,6 @@ export default function ArtifactTabs({
|
|||
startupConfig={startupConfig}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export default function ArtifactVersion({
|
|||
return (
|
||||
<DropdownPopup
|
||||
menuId={menuId}
|
||||
portal
|
||||
focusLoop
|
||||
unmountOnHide
|
||||
isOpen={isPopoverActive}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useRef, useState, useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { Code, Play, RefreshCw, X } from 'lucide-react';
|
||||
import { useSetRecoilState, useResetRecoilState } from 'recoil';
|
||||
import { Button, Spinner, useMediaQuery } from '@librechat/client';
|
||||
import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-react';
|
||||
import useArtifacts from '~/hooks/Artifacts/useArtifacts';
|
||||
|
|
@ -21,12 +21,49 @@ export default function Artifacts() {
|
|||
const editorRef = useRef<CodeEditorRef>();
|
||||
const previewRef = useRef<SandpackPreviewRef>();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [height, setHeight] = useState(90); // Height as percentage of viewport
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [blurAmount, setBlurAmount] = useState(0); // Dynamic blur amount
|
||||
const dragStartY = useRef(0);
|
||||
const dragStartHeight = useRef(90);
|
||||
const setArtifactsVisible = useSetRecoilState(store.artifactsVisibility);
|
||||
const resetCurrentArtifactId = useResetRecoilState(store.currentArtifactId);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(true);
|
||||
}, []);
|
||||
setIsMounted(true);
|
||||
const delay = isMobile ? 50 : 30;
|
||||
const timer = setTimeout(() => setIsVisible(true), delay);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
setIsMounted(false);
|
||||
};
|
||||
}, [isMobile]);
|
||||
|
||||
// Dynamic blur based on height - more blur when taking up more screen
|
||||
useEffect(() => {
|
||||
if (!isMobile) {
|
||||
setBlurAmount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate blur amount based on how much screen is covered
|
||||
// 50% height = no blur, 100% height = full blur
|
||||
const minHeightForBlur = 50;
|
||||
const maxHeightForBlur = 100;
|
||||
|
||||
if (height <= minHeightForBlur) {
|
||||
setBlurAmount(0);
|
||||
} else if (height >= maxHeightForBlur) {
|
||||
setBlurAmount(32); // Increased from 16 to 32 for stronger blur
|
||||
} else {
|
||||
// Linear interpolation between 0 and 32px blur
|
||||
const progress = (height - minHeightForBlur) / (maxHeightForBlur - minHeightForBlur);
|
||||
setBlurAmount(Math.round(progress * 32)); // Changed from 16 to 32
|
||||
}
|
||||
}, [height, isMobile]);
|
||||
|
||||
const {
|
||||
activeTab,
|
||||
|
|
@ -37,7 +74,47 @@ export default function Artifacts() {
|
|||
setCurrentArtifactId,
|
||||
} = useArtifacts();
|
||||
|
||||
if (!currentArtifact) {
|
||||
const handleDragStart = (e: React.PointerEvent) => {
|
||||
setIsDragging(true);
|
||||
dragStartY.current = e.clientY;
|
||||
dragStartHeight.current = height;
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
};
|
||||
|
||||
const handleDragMove = (e: React.PointerEvent) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const deltaY = dragStartY.current - e.clientY;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const deltaPercentage = (deltaY / viewportHeight) * 100;
|
||||
const newHeight = Math.max(10, Math.min(100, dragStartHeight.current + deltaPercentage));
|
||||
|
||||
setHeight(newHeight);
|
||||
};
|
||||
|
||||
const handleDragEnd = (e: React.PointerEvent) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
setIsDragging(false);
|
||||
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
|
||||
|
||||
// Snap to positions based on final height
|
||||
if (height < 30) {
|
||||
// Close if dragged down significantly
|
||||
closeArtifacts();
|
||||
} else if (height > 95) {
|
||||
// Snap to full height if dragged near top
|
||||
setHeight(100);
|
||||
} else if (height < 60) {
|
||||
// Snap to minimum if in lower range
|
||||
setHeight(50);
|
||||
} else {
|
||||
// Snap to default
|
||||
setHeight(90);
|
||||
}
|
||||
};
|
||||
|
||||
if (!currentArtifact || !isMounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -51,57 +128,97 @@ export default function Artifacts() {
|
|||
};
|
||||
|
||||
const closeArtifacts = () => {
|
||||
setIsVisible(false);
|
||||
setTimeout(() => setArtifactsVisible(false), isMobile ? 400 : 500);
|
||||
if (isMobile) {
|
||||
setIsClosing(true);
|
||||
setIsVisible(false);
|
||||
setTimeout(() => {
|
||||
setArtifactsVisible(false);
|
||||
setIsClosing(false);
|
||||
setHeight(90); // Reset height
|
||||
}, 250);
|
||||
} else {
|
||||
resetCurrentArtifactId();
|
||||
setArtifactsVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate backdrop opacity based on blur amount
|
||||
const backdropOpacity = blurAmount > 0 ? Math.min(0.3, blurAmount / 53.33) : 0;
|
||||
|
||||
return (
|
||||
<Tabs.Root value={activeTab} onValueChange={setActiveTab} asChild>
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{/* Mobile backdrop */}
|
||||
{/* Mobile backdrop with dynamic blur */}
|
||||
{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',
|
||||
'fixed inset-0 z-[99] bg-black will-change-[opacity,backdrop-filter]',
|
||||
isVisible && !isClosing
|
||||
? 'transition-all duration-300'
|
||||
: 'pointer-events-none opacity-0 backdrop-blur-none transition-opacity duration-150',
|
||||
// Allow pointer events when not fully blurred so chat is scrollable
|
||||
blurAmount < 8 && isVisible && !isClosing ? 'pointer-events-none' : '',
|
||||
)}
|
||||
onClick={closeArtifacts}
|
||||
style={{
|
||||
opacity: isVisible && !isClosing ? backdropOpacity : 0,
|
||||
backdropFilter: isVisible && !isClosing ? `blur(${blurAmount}px)` : 'none',
|
||||
WebkitBackdropFilter: isVisible && !isClosing ? `blur(${blurAmount}px)` : 'none',
|
||||
}}
|
||||
onClick={blurAmount >= 8 ? closeArtifacts : undefined}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col overflow-hidden bg-surface-primary text-xl text-text-primary',
|
||||
'flex w-full flex-col 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',
|
||||
'fixed inset-x-0 bottom-0 z-[100] rounded-t-[20px] shadow-[0_-10px_60px_rgba(0,0,0,0.35)]',
|
||||
isVisible && !isClosing
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'duration-250 translate-y-full opacity-0 transition-all',
|
||||
isDragging ? '' : 'transition-all duration-300',
|
||||
)
|
||||
: 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',
|
||||
'h-full shadow-2xl',
|
||||
isVisible && !isClosing
|
||||
? 'duration-350 translate-x-0 opacity-100 transition-all'
|
||||
: 'translate-x-5 opacity-0 transition-all duration-300',
|
||||
),
|
||||
)}
|
||||
style={isMobile ? { height: `${height}vh` } : { overflow: 'hidden' }}
|
||||
>
|
||||
{/* 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
|
||||
className="flex flex-shrink-0 cursor-grab items-center justify-center bg-surface-primary-alt pb-1.5 pt-2.5 active:cursor-grabbing"
|
||||
onPointerDown={handleDragStart}
|
||||
onPointerMove={handleDragMove}
|
||||
onPointerUp={handleDragEnd}
|
||||
onPointerCancel={handleDragEnd}
|
||||
>
|
||||
<div className="h-1 w-12 rounded-full bg-border-xheavy opacity-40 transition-all duration-200 active:opacity-60" />
|
||||
</div>
|
||||
)}
|
||||
{/* Header */}
|
||||
<div
|
||||
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',
|
||||
'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',
|
||||
isMobile ? 'justify-center' : 'overflow-hidden',
|
||||
)}
|
||||
>
|
||||
{!isMobile && (
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center transition-all duration-500',
|
||||
isVisible && !isClosing
|
||||
? 'translate-x-0 opacity-100'
|
||||
: '-translate-x-2 opacity-0',
|
||||
)}
|
||||
>
|
||||
<Tabs.List className="relative inline-flex h-9 gap-2 rounded-xl bg-surface-tertiary p-0.5">
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-0.5 h-8 rounded-xl bg-surface-primary-alt transition-transform duration-200 ease-out',
|
||||
'duration-[350ms] absolute top-0.5 h-8 rounded-[10px] bg-surface-primary-alt shadow-sm transition-all will-change-transform',
|
||||
activeTab === 'code'
|
||||
? 'w-[42%] translate-x-0'
|
||||
: 'w-[50%] translate-x-[calc(100%-0.250rem)]',
|
||||
|
|
@ -109,24 +226,30 @@ export default function Artifacts() {
|
|||
/>
|
||||
<Tabs.Trigger
|
||||
value="code"
|
||||
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"
|
||||
className="relative z-10 flex items-center gap-1.5 rounded-[10px] border-transparent px-3 py-1 text-xs font-medium transition-all duration-200 hover:text-text-primary active:scale-95 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
>
|
||||
<Code className="size-3" />
|
||||
<span>{localize('com_ui_code')}</span>
|
||||
<Code className="size-3 transition-transform duration-200 group-active:scale-90" />
|
||||
<span className="whitespace-nowrap">{localize('com_ui_code')}</span>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="preview"
|
||||
disabled={isMutating}
|
||||
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"
|
||||
className="relative z-10 flex items-center gap-1.5 rounded-[10px] border-transparent px-3 py-1 text-xs font-medium transition-all duration-200 hover:text-text-primary active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
>
|
||||
<Play className="size-3" />
|
||||
<span>{localize('com_ui_preview')}</span>
|
||||
<Play className="size-3 transition-transform duration-200 group-active:scale-90" />
|
||||
<span className="whitespace-nowrap">{localize('com_ui_preview')}</span>
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex min-w-max items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 transition-all duration-500',
|
||||
isMobile ? 'min-w-max' : '',
|
||||
isVisible && !isClosing ? 'translate-x-0 opacity-100' : 'translate-x-2 opacity-0',
|
||||
)}
|
||||
>
|
||||
{activeTab === 'preview' && (
|
||||
<Button
|
||||
size="icon"
|
||||
|
|
@ -134,9 +257,13 @@ export default function Artifacts() {
|
|||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
aria-label={localize('com_ui_refresh')}
|
||||
className="h-8 w-8 transition-transform duration-150 ease-out hover:scale-105 active:scale-95"
|
||||
className="transition-all duration-150 active:scale-90"
|
||||
>
|
||||
{isRefreshing ? <Spinner size={16} /> : <RefreshCw size={16} />}
|
||||
{isRefreshing ? (
|
||||
<Spinner size={16} />
|
||||
) : (
|
||||
<RefreshCw size={16} className="transition-transform duration-200" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{activeTab !== 'preview' && isMutating && (
|
||||
|
|
@ -161,35 +288,37 @@ export default function Artifacts() {
|
|||
variant="ghost"
|
||||
onClick={closeArtifacts}
|
||||
aria-label={localize('com_ui_close')}
|
||||
className="h-8 w-8 transition-transform duration-150 ease-out hover:scale-105 active:scale-95"
|
||||
className="h-8 w-8 transition-all duration-150 hover:scale-105 active:scale-90"
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Area - This is the key fix */}
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<ArtifactTabs
|
||||
artifact={currentArtifact}
|
||||
editorRef={editorRef as React.MutableRefObject<CodeEditorRef>}
|
||||
previewRef={previewRef as React.MutableRefObject<SandpackPreviewRef>}
|
||||
/>
|
||||
{/* Content Area - Fixed positioning to prevent layout shifts */}
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden bg-surface-primary">
|
||||
<div className="absolute inset-0 flex flex-col">
|
||||
<ArtifactTabs
|
||||
artifact={currentArtifact}
|
||||
editorRef={editorRef as React.MutableRefObject<CodeEditorRef>}
|
||||
previewRef={previewRef as React.MutableRefObject<SandpackPreviewRef>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Tab Switcher */}
|
||||
{/* Mobile Tab Switcher with iOS-style animation */}
|
||||
{isMobile && (
|
||||
<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={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',
|
||||
'duration-[350ms] absolute left-1 top-1 h-8 w-[calc(50%-0.25rem)] rounded-[10px] bg-surface-primary-alt shadow-sm transition-transform will-change-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 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"
|
||||
className="relative z-10 flex w-1/2 items-center justify-center gap-1.5 rounded-[10px] border-transparent py-1.5 text-xs font-medium transition-all duration-150 active:scale-95 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
>
|
||||
<Code className="size-3.5" />
|
||||
<span>{localize('com_ui_code')}</span>
|
||||
|
|
@ -197,7 +326,7 @@ export default function Artifacts() {
|
|||
<Tabs.Trigger
|
||||
value="preview"
|
||||
disabled={isMutating}
|
||||
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"
|
||||
className="relative z-10 flex w-1/2 items-center justify-center gap-1.5 rounded-[10px] border-transparent py-1.5 text-xs font-medium transition-all duration-150 active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||
>
|
||||
<Play className="size-3.5" />
|
||||
<span>{localize('com_ui_preview')}</span>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { cn } from '~/utils';
|
|||
import store from '~/store';
|
||||
|
||||
const BUTTON_STYLES = {
|
||||
base: 'group mt-3 flex w-fit items-center justify-center rounded-xl bg-surface-tertiary px-3 py-2 text-xs leading-[18px] animate-thinking-appear',
|
||||
icon: 'icon-sm ml-1.5 transform-gpu text-text-primary transition-transform duration-200',
|
||||
base: '-sring group mt-3 flex w-fit items-center justify-center rounded-xl bg-surface-tertiary px-3 py-2 text-xs leading-[18px] transition-all duration-300 hover:bg-surface-secondary active:scale-95 animate-thinking-appear',
|
||||
icon: 'icon-sm ml-1.5 transform-gpu text-text-primary transition-transform duration-300',
|
||||
} as const;
|
||||
|
||||
const CONTENT_STYLES = {
|
||||
|
|
@ -69,7 +69,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
|
|||
<ThinkingButton isExpanded={isExpanded} onClick={handleClick} label={label} />
|
||||
</div>
|
||||
<div
|
||||
className={cn('grid transition-all duration-300 ease-out', isExpanded && 'mb-8')}
|
||||
className={cn('duration-400 grid transition-all', isExpanded && 'mb-8')}
|
||||
style={{
|
||||
gridTemplateRows: isExpanded ? '1fr' : '0fr',
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import { normalizeLayout } from '~/utils';
|
|||
import SidePanel from './SidePanel';
|
||||
import store from '~/store';
|
||||
|
||||
const ANIMATION_DURATION = 500;
|
||||
|
||||
interface SidePanelProps {
|
||||
defaultLayout?: number[] | undefined;
|
||||
defaultCollapsed?: boolean;
|
||||
|
|
@ -42,14 +44,43 @@ const SidePanelGroup = memo(
|
|||
);
|
||||
|
||||
const panelRef = useRef<ImperativePanelHandle>(null);
|
||||
const artifactsPanelRef = useRef<ImperativePanelHandle>(null);
|
||||
const [minSize, setMinSize] = useState(defaultMinSize);
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
|
||||
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
|
||||
const [shouldRenderArtifacts, setShouldRenderArtifacts] = useState(artifacts != null);
|
||||
const artifactsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||
const hideSidePanel = useRecoilValue(store.hideSidePanel);
|
||||
|
||||
useEffect(() => {
|
||||
if (artifacts != null) {
|
||||
if (artifactsTimeoutRef.current) {
|
||||
clearTimeout(artifactsTimeoutRef.current);
|
||||
artifactsTimeoutRef.current = null;
|
||||
}
|
||||
setShouldRenderArtifacts(true);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
artifactsPanelRef.current?.expand();
|
||||
});
|
||||
});
|
||||
} else if (shouldRenderArtifacts) {
|
||||
artifactsPanelRef.current?.collapse();
|
||||
artifactsTimeoutRef.current = setTimeout(() => {
|
||||
setShouldRenderArtifacts(false);
|
||||
}, ANIMATION_DURATION);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (artifactsTimeoutRef.current) {
|
||||
clearTimeout(artifactsTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [artifacts, shouldRenderArtifacts]);
|
||||
|
||||
const calculateLayout = useCallback(() => {
|
||||
if (artifacts == null) {
|
||||
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
|
||||
|
|
@ -120,20 +151,25 @@ const SidePanelGroup = memo(
|
|||
>
|
||||
{children}
|
||||
</ResizablePanel>
|
||||
{artifacts != null && !isSmallScreen && (
|
||||
{shouldRenderArtifacts && !isSmallScreen && (
|
||||
<>
|
||||
<ResizableHandleAlt
|
||||
withHandle
|
||||
className="ml-3 bg-border-medium text-text-primary transition-opacity duration-300"
|
||||
/>
|
||||
{artifacts != null && (
|
||||
<ResizableHandleAlt
|
||||
withHandle
|
||||
className="ml-3 bg-border-medium text-text-primary"
|
||||
/>
|
||||
)}
|
||||
<ResizablePanel
|
||||
defaultSize={currentLayout[1]}
|
||||
ref={artifactsPanelRef}
|
||||
defaultSize={artifacts != null ? currentLayout[1] : 0}
|
||||
minSize={minSizeMain}
|
||||
collapsible={true}
|
||||
collapsedSize={0}
|
||||
order={2}
|
||||
id="artifacts-panel"
|
||||
className="ease-[cubic-bezier(0.25,0.46,0.45,0.94)] transition-all duration-500"
|
||||
>
|
||||
{artifacts}
|
||||
<div className="h-full min-w-[400px] overflow-hidden">{artifacts}</div>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -427,7 +427,7 @@
|
|||
"com_ui_all_proper": "الكل",
|
||||
"com_ui_archive": "أرشفة",
|
||||
"com_ui_archive_error": "فشل في أرشفة المحادثة",
|
||||
"com_ui_artifact_click": "انقر للفتح",
|
||||
"com_ui_click_to_open": "انقر للفتح",
|
||||
"com_ui_artifacts": "المخرجات",
|
||||
"com_ui_artifacts_toggle": "تبديل واجهة العناصر",
|
||||
"com_ui_ascending": "تصاعدي",
|
||||
|
|
|
|||
|
|
@ -508,7 +508,7 @@
|
|||
"com_ui_archive": "Arxiva",
|
||||
"com_ui_archive_delete_error": "No s'ha pogut eliminar la conversa arxivada",
|
||||
"com_ui_archive_error": "No s'ha pogut arxivar la conversa",
|
||||
"com_ui_artifact_click": "Fes clic per obrir",
|
||||
"com_ui_click_to_open": "Fes clic per obrir",
|
||||
"com_ui_artifacts": "Artifacts",
|
||||
"com_ui_artifacts_toggle": "Activa/desactiva la UI d'artifacts",
|
||||
"com_ui_artifacts_toggle_agent": "Habilita artifacts",
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@
|
|||
"com_ui_api_key": "API klíč",
|
||||
"com_ui_archive": "Archivovat",
|
||||
"com_ui_archive_error": "Nepodařilo se archivovat konverzaci",
|
||||
"com_ui_artifact_click": "Klikněte pro otevření",
|
||||
"com_ui_click_to_open": "Klikněte pro otevření",
|
||||
"com_ui_artifacts": "Artefakty",
|
||||
"com_ui_artifacts_toggle": "Přepnout uživatelské rozhraní artefaktů",
|
||||
"com_ui_artifacts_toggle_agent": "Povolit artefakty",
|
||||
|
|
|
|||
|
|
@ -531,7 +531,7 @@
|
|||
"com_ui_archive": "Arkiv",
|
||||
"com_ui_archive_delete_error": "Kunne ikke slette arkiveret samtale",
|
||||
"com_ui_archive_error": "Kunne ikke arkivere samtale",
|
||||
"com_ui_artifact_click": "Klik for at åbne",
|
||||
"com_ui_click_to_open": "Klik for at åbne",
|
||||
"com_ui_artifacts": "Artefakter",
|
||||
"com_ui_artifacts_toggle": "Skift artefakter UI",
|
||||
"com_ui_artifacts_toggle_agent": "Aktiver artefakter",
|
||||
|
|
|
|||
|
|
@ -685,7 +685,7 @@
|
|||
"com_ui_archive": "Archivieren",
|
||||
"com_ui_archive_delete_error": "Archivierter Chat konnte nicht gelöscht werden.",
|
||||
"com_ui_archive_error": "Konversation konnte nicht archiviert werden",
|
||||
"com_ui_artifact_click": "Zum Öffnen klicken",
|
||||
"com_ui_click_to_open": "Zum Öffnen klicken",
|
||||
"com_ui_artifacts": "Artefakte",
|
||||
"com_ui_artifacts_options": "Artefakt Optionen",
|
||||
"com_ui_artifacts_toggle": "Artefakte-Funktion einschalten",
|
||||
|
|
|
|||
|
|
@ -688,7 +688,8 @@
|
|||
"com_ui_archive": "Archive",
|
||||
"com_ui_archive_delete_error": "Failed to delete archived conversation",
|
||||
"com_ui_archive_error": "Failed to archive conversation",
|
||||
"com_ui_artifact_click": "Click to open",
|
||||
"com_ui_click_to_open": "Click to open",
|
||||
"com_ui_click_to_close": "Click to close",
|
||||
"com_ui_artifacts": "Artifacts",
|
||||
"com_ui_artifacts_options": "Artifacts Options",
|
||||
"com_ui_artifacts_toggle": "Toggle Artifacts UI",
|
||||
|
|
|
|||
|
|
@ -510,7 +510,7 @@
|
|||
"com_ui_archive": "Archivar",
|
||||
"com_ui_archive_delete_error": "Error al borrar conversación archivada",
|
||||
"com_ui_archive_error": "Error al archivar la conversación",
|
||||
"com_ui_artifact_click": "Haga clic para abrir",
|
||||
"com_ui_click_to_open": "Haga clic para abrir",
|
||||
"com_ui_artifacts": "Artefactos",
|
||||
"com_ui_artifacts_options": "Opciones de artefactos",
|
||||
"com_ui_artifacts_toggle": "Alternar Interfaz de Artefactos",
|
||||
|
|
|
|||
|
|
@ -532,7 +532,7 @@
|
|||
"com_ui_archive": "Arhiveeri",
|
||||
"com_ui_archive_delete_error": "Arhiveeritud vestluse kustutamine ebaõnnestus",
|
||||
"com_ui_archive_error": "Vestluse arhiveerimine ebaõnnestus",
|
||||
"com_ui_artifact_click": "Klõpsa avamiseks",
|
||||
"com_ui_click_to_open": "Klõpsa avamiseks",
|
||||
"com_ui_artifacts": "Artefaktid",
|
||||
"com_ui_artifacts_toggle": "Lülita artefaktide kasutajaliides sisse/välja",
|
||||
"com_ui_artifacts_toggle_agent": "Luba artefaktid",
|
||||
|
|
|
|||
|
|
@ -479,7 +479,7 @@
|
|||
"com_ui_api_key": "کلید API",
|
||||
"com_ui_archive": "آرشیو",
|
||||
"com_ui_archive_error": "مکالمه بایگانی نشد",
|
||||
"com_ui_artifact_click": "برای باز کردن کلیک کنید",
|
||||
"com_ui_click_to_open": "برای باز کردن کلیک کنید",
|
||||
"com_ui_artifacts": "مصنوعات",
|
||||
"com_ui_artifacts_toggle": "تغییر رابط کاربری Artifacts",
|
||||
"com_ui_artifacts_toggle_agent": "Artifacts را فعال کنید",
|
||||
|
|
|
|||
|
|
@ -635,7 +635,7 @@
|
|||
"com_ui_archive": "Archiver",
|
||||
"com_ui_archive_delete_error": "Suppression de la conversation archivée échouée",
|
||||
"com_ui_archive_error": "échec de l'archivage de la conversation",
|
||||
"com_ui_artifact_click": "Cliquer pour ouvrir",
|
||||
"com_ui_click_to_open": "Cliquer pour ouvrir",
|
||||
"com_ui_artifacts": "Artefacts",
|
||||
"com_ui_artifacts_options": "Options des Artefacts",
|
||||
"com_ui_artifacts_toggle": "Afficher/Masquer l'interface des artefacts",
|
||||
|
|
|
|||
|
|
@ -676,7 +676,7 @@
|
|||
"com_ui_archive": "לארכיון",
|
||||
"com_ui_archive_delete_error": "מחיקת השיחה מהארכיון נכשלה",
|
||||
"com_ui_archive_error": "אירעה שגיאה באירכוב השיחה",
|
||||
"com_ui_artifact_click": "לחץ לפתיחה",
|
||||
"com_ui_click_to_open": "לחץ לפתיחה",
|
||||
"com_ui_artifacts": "רכיבי תצוגה",
|
||||
"com_ui_artifacts_options": "אפשרויות ארטיפקטים",
|
||||
"com_ui_artifacts_toggle": "הפעל/כבה רכיבי תצוגה",
|
||||
|
|
|
|||
|
|
@ -479,7 +479,7 @@
|
|||
"com_ui_api_key": "API kulcs",
|
||||
"com_ui_archive": "Archiválás",
|
||||
"com_ui_archive_error": "Nem sikerült archiválni a beszélgetést",
|
||||
"com_ui_artifact_click": "Kattintson a megnyitáshoz",
|
||||
"com_ui_click_to_open": "Kattintson a megnyitáshoz",
|
||||
"com_ui_artifacts": "Műtermékek",
|
||||
"com_ui_artifacts_toggle": "Műtermék kezelőfelület váltása",
|
||||
"com_ui_artifacts_toggle_agent": "Műtermékek engedélyezése",
|
||||
|
|
|
|||
|
|
@ -541,7 +541,7 @@
|
|||
"com_ui_api_key": "Chiave API",
|
||||
"com_ui_archive": "Archivia",
|
||||
"com_ui_archive_error": "Errore durante l'archiviazione della conversazione",
|
||||
"com_ui_artifact_click": "Clicca per aprire",
|
||||
"com_ui_click_to_open": "Clicca per aprire",
|
||||
"com_ui_artifacts": "Artefatti",
|
||||
"com_ui_artifacts_toggle": "Mostra/Nascondi Interfaccia Artefatti",
|
||||
"com_ui_artifacts_toggle_agent": "Abilita artefatti",
|
||||
|
|
|
|||
|
|
@ -576,7 +576,7 @@
|
|||
"com_ui_archive": "アーカイブ",
|
||||
"com_ui_archive_delete_error": "アーカイブされた会話の削除に失敗しました",
|
||||
"com_ui_archive_error": "アーカイブに失敗しました。",
|
||||
"com_ui_artifact_click": "クリックして開く",
|
||||
"com_ui_click_to_open": "クリックして開く",
|
||||
"com_ui_artifacts": "アーティファクト",
|
||||
"com_ui_artifacts_options": "アーティファクト・オプション",
|
||||
"com_ui_artifacts_toggle": "アーティファクト UI の切替",
|
||||
|
|
|
|||
|
|
@ -567,7 +567,7 @@
|
|||
"com_ui_archive": "아카이브",
|
||||
"com_ui_archive_delete_error": "저장된 대화 삭제 실패",
|
||||
"com_ui_archive_error": "대화 아카이브 실패",
|
||||
"com_ui_artifact_click": "클릭하여 열기",
|
||||
"com_ui_click_to_open": "클릭하여 열기",
|
||||
"com_ui_artifacts": "아티팩트",
|
||||
"com_ui_artifacts_options": "아티팩트 옵션",
|
||||
"com_ui_artifacts_toggle": "아티팩트 UI 표시/숨기기",
|
||||
|
|
|
|||
|
|
@ -688,7 +688,7 @@
|
|||
"com_ui_archive": "Arhīvs",
|
||||
"com_ui_archive_delete_error": "Neizdevās izdzēst arhivēto sarunu.",
|
||||
"com_ui_archive_error": "Neizdevās arhivēt sarunu.",
|
||||
"com_ui_artifact_click": "Noklikšķiniet, lai atvērtu",
|
||||
"com_ui_click_to_open": "Noklikšķiniet, lai atvērtu",
|
||||
"com_ui_artifacts": "Artefakti",
|
||||
"com_ui_artifacts_options": "Artefaktu opcijas",
|
||||
"com_ui_artifacts_toggle": "Pārslēgt artefaktu lietotāja saskarni",
|
||||
|
|
|
|||
|
|
@ -683,7 +683,7 @@
|
|||
"com_ui_archive": "Arkiver",
|
||||
"com_ui_archive_delete_error": "Sletting av arkivert samtale mislyktes.",
|
||||
"com_ui_archive_error": "Arkivering av samtale mislyktes.",
|
||||
"com_ui_artifact_click": "Klikk for å åpne",
|
||||
"com_ui_click_to_open": "Klikk for å åpne",
|
||||
"com_ui_artifacts": "Artefakter",
|
||||
"com_ui_artifacts_options": "Alternativer for artefakter",
|
||||
"com_ui_artifacts_toggle": "Veksle artefakt-UI",
|
||||
|
|
|
|||
|
|
@ -429,7 +429,7 @@
|
|||
"com_ui_all_proper": "Wszystkie",
|
||||
"com_ui_archive": "Archiwum",
|
||||
"com_ui_archive_error": "Nie udało się archiwizować rozmowy",
|
||||
"com_ui_artifact_click": "Kliknij, aby otworzyć",
|
||||
"com_ui_click_to_open": "Kliknij, aby otworzyć",
|
||||
"com_ui_artifacts": "Artefakty",
|
||||
"com_ui_artifacts_toggle": "Przełącz interfejs artefaktów",
|
||||
"com_ui_ascending": "Rosnąco",
|
||||
|
|
|
|||
|
|
@ -582,7 +582,7 @@
|
|||
"com_ui_api_key": "Chave API",
|
||||
"com_ui_archive": "Arquivar",
|
||||
"com_ui_archive_error": "Falha ao arquivar conversa",
|
||||
"com_ui_artifact_click": "Clique para abrir",
|
||||
"com_ui_click_to_open": "Clique para abrir",
|
||||
"com_ui_artifacts": "Artefatos",
|
||||
"com_ui_artifacts_toggle": "Alternar UI de Artefatos",
|
||||
"com_ui_artifacts_toggle_agent": "Habilitar artefatos",
|
||||
|
|
|
|||
|
|
@ -656,7 +656,7 @@
|
|||
"com_ui_archive": "Arquivar",
|
||||
"com_ui_archive_delete_error": "Falha ao eliminar conversa arquivada",
|
||||
"com_ui_archive_error": "Falha ao arquivar conversa",
|
||||
"com_ui_artifact_click": "Clique para abrir",
|
||||
"com_ui_click_to_open": "Clique para abrir",
|
||||
"com_ui_artifacts": "Artefatos",
|
||||
"com_ui_artifacts_options": "Opções de Artefactos",
|
||||
"com_ui_artifacts_toggle": "Alternar UI de Artefatos",
|
||||
|
|
|
|||
|
|
@ -682,7 +682,7 @@
|
|||
"com_ui_archive": "Архивировать",
|
||||
"com_ui_archive_delete_error": "Не удалось удалить архивированный чат",
|
||||
"com_ui_archive_error": "Не удалось заархивировать чат",
|
||||
"com_ui_artifact_click": "Нажмите, чтобы открыть",
|
||||
"com_ui_click_to_open": "Нажмите, чтобы открыть",
|
||||
"com_ui_artifacts": "Артефакты",
|
||||
"com_ui_artifacts_options": "Параметры артефактов",
|
||||
"com_ui_artifacts_toggle": "Показать/скрыть артефакты",
|
||||
|
|
|
|||
|
|
@ -477,7 +477,7 @@
|
|||
"com_ui_api_key": "คีย์ API",
|
||||
"com_ui_archive": "เก็บถาวร",
|
||||
"com_ui_archive_error": "ไม่สามารถเก็บถาวรการสนทนา",
|
||||
"com_ui_artifact_click": "คลิกเพื่อเปิด",
|
||||
"com_ui_click_to_open": "คลิกเพื่อเปิด",
|
||||
"com_ui_artifacts": "สิ่งประดิษฐ์",
|
||||
"com_ui_artifacts_toggle": "สลับ UI สิ่งประดิษฐ์",
|
||||
"com_ui_artifacts_toggle_agent": "เปิดใช้งานสิ่งประดิษฐ์",
|
||||
|
|
|
|||
|
|
@ -430,7 +430,7 @@
|
|||
"com_ui_all_proper": "Tümü",
|
||||
"com_ui_archive": "Arşivle",
|
||||
"com_ui_archive_error": "Konuşmayı arşivleyemedi",
|
||||
"com_ui_artifact_click": "Açmak için tıklayın",
|
||||
"com_ui_click_to_open": "Açmak için tıklayın",
|
||||
"com_ui_artifacts": "Yapıtlar",
|
||||
"com_ui_artifacts_toggle": "Yapıtlar Arayüzünü Aç/Kapat",
|
||||
"com_ui_ascending": "Artan",
|
||||
|
|
|
|||
|
|
@ -683,7 +683,7 @@
|
|||
"com_ui_archive": "Архівувати",
|
||||
"com_ui_archive_delete_error": "Не вдалося видалити заархівований чат",
|
||||
"com_ui_archive_error": "Не вдалося заархівувати чат",
|
||||
"com_ui_artifact_click": "Натисніть, щоб відкрити",
|
||||
"com_ui_click_to_open": "Натисніть, щоб відкрити",
|
||||
"com_ui_artifacts": "Артефакти",
|
||||
"com_ui_artifacts_options": "Параметри артефактів",
|
||||
"com_ui_artifacts_toggle": "Показати/приховати артефакти",
|
||||
|
|
|
|||
|
|
@ -686,7 +686,7 @@
|
|||
"com_ui_archive": "归档",
|
||||
"com_ui_archive_delete_error": "删除已归档对话失败",
|
||||
"com_ui_archive_error": "归档对话失败",
|
||||
"com_ui_artifact_click": "点击以打开",
|
||||
"com_ui_click_to_open": "点击以打开",
|
||||
"com_ui_artifacts": "Artifacts",
|
||||
"com_ui_artifacts_options": "Artifacts 选项",
|
||||
"com_ui_artifacts_toggle": "切换 Artifacts UI",
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@
|
|||
"com_ui_api_key": "API 金鑰",
|
||||
"com_ui_archive": "封存",
|
||||
"com_ui_archive_error": "封存對話時發生錯誤",
|
||||
"com_ui_artifact_click": "點擊開啟",
|
||||
"com_ui_click_to_open": "點擊開啟",
|
||||
"com_ui_artifacts": "成品",
|
||||
"com_ui_artifacts_toggle": "切換成品介面",
|
||||
"com_ui_ascending": "遞增",
|
||||
|
|
|
|||
|
|
@ -2715,6 +2715,8 @@ html {
|
|||
.animate-pulse-slow {
|
||||
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* iOS-inspired smooth animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
|
@ -2730,10 +2732,45 @@ html {
|
|||
animation: fadeIn 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Enhanced smooth scaling for interactions */
|
||||
.scale-98 {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Add hardware acceleration for smoother animations */
|
||||
.will-change-transform {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.will-change-opacity {
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
/* Prevent content flash and layout shifts in artifacts */
|
||||
[data-radix-scroll-area-viewport] {
|
||||
/* Prevent scrollbar from causing layout shifts */
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
/* Ensure smooth tab content transitions */
|
||||
[role="tabpanel"] {
|
||||
/* Use GPU acceleration for tab content */
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
|
||||
/* Prevent flash of content during mounting */
|
||||
[role="tabpanel"][data-state="inactive"] {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[role="tabpanel"][data-state="active"] {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Chat Badges Animation */
|
||||
|
||||
@keyframes ios-wiggle {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,69 @@ module.exports = {
|
|||
'0%': { transform: 'translateX(0)' },
|
||||
'100%': { transform: 'translateX(100%)' },
|
||||
},
|
||||
// iOS-inspired smooth animations
|
||||
'artifact-slide-up': {
|
||||
'0%': {
|
||||
transform: 'translateY(100%) scale(0.95)',
|
||||
opacity: '0',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateY(0) scale(1)',
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
'artifact-slide-down': {
|
||||
'0%': {
|
||||
transform: 'translateY(0) scale(1)',
|
||||
opacity: '1',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateY(100%) scale(0.95)',
|
||||
opacity: '0',
|
||||
},
|
||||
},
|
||||
'artifact-slide-in-desktop': {
|
||||
'0%': {
|
||||
transform: 'translateX(20px)',
|
||||
opacity: '0',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(0)',
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
'artifact-slide-out-desktop': {
|
||||
'0%': {
|
||||
transform: 'translateX(0)',
|
||||
opacity: '1',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(20px)',
|
||||
opacity: '0',
|
||||
},
|
||||
},
|
||||
'backdrop-fade-in': {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
'backdrop-fade-out': {
|
||||
'0%': { opacity: '1' },
|
||||
'100%': { opacity: '0' },
|
||||
},
|
||||
'tab-slide': {
|
||||
'0%': { transform: 'translateX(var(--tab-slide-from))' },
|
||||
'100%': { transform: 'translateX(var(--tab-slide-to))' },
|
||||
},
|
||||
'thinking-appear': {
|
||||
'0%': {
|
||||
opacity: '0',
|
||||
transform: 'scale(0.9) translateY(4px)',
|
||||
},
|
||||
'100%': {
|
||||
opacity: '1',
|
||||
transform: 'scale(1) translateY(0)',
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.5s ease-out forwards',
|
||||
|
|
@ -56,6 +119,22 @@ module.exports = {
|
|||
'slide-in-left': 'slide-in-left 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
|
||||
'slide-out-left': 'slide-out-left 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
|
||||
'slide-out-right': 'slide-out-right 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
|
||||
// iOS-inspired smooth animations
|
||||
'artifact-slide-up': 'artifact-slide-up 0.45s cubic-bezier(0.32, 0.72, 0, 1)',
|
||||
'artifact-slide-down': 'artifact-slide-down 0.35s cubic-bezier(0.32, 0.72, 0, 1)',
|
||||
'artifact-slide-in-desktop':
|
||||
'artifact-slide-in-desktop 0.5s cubic-bezier(0.32, 0.72, 0, 1)',
|
||||
'artifact-slide-out-desktop':
|
||||
'artifact-slide-out-desktop 0.35s cubic-bezier(0.32, 0.72, 0, 1)',
|
||||
'backdrop-fade-in': 'backdrop-fade-in 0.3s cubic-bezier(0.32, 0.72, 0, 1)',
|
||||
'backdrop-fade-out': 'backdrop-fade-out 0.25s cubic-bezier(0.32, 0.72, 0, 1)',
|
||||
'tab-slide': 'tab-slide 0.35s cubic-bezier(0.32, 0.72, 0, 1)',
|
||||
'thinking-appear': 'thinking-appear 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
ios: 'cubic-bezier(0.32, 0.72, 0, 1)',
|
||||
'ios-spring': 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
'ios-decelerate': 'cubic-bezier(0, 0, 0.2, 1)',
|
||||
},
|
||||
colors: {
|
||||
gray: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue