Compare commits
3 Commits
fix/gemini
...
feat/messa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7e79353fc | ||
|
|
93e972a5da | ||
|
|
12d08e4748 |
@@ -163,6 +163,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
@keyframes fadeInWithBlur {
|
||||||
|
from { opacity: 0; filter: blur(2px); }
|
||||||
|
to { opacity: 1; filter: blur(0px); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { removeSvgEmptyLines } from '@renderer/utils/formats'
|
|||||||
import { processLatexBrackets } from '@renderer/utils/markdown'
|
import { processLatexBrackets } from '@renderer/utils/markdown'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { type FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { type FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown'
|
import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown'
|
||||||
import rehypeKatex from 'rehype-katex'
|
import rehypeKatex from 'rehype-katex'
|
||||||
@@ -64,6 +65,8 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
|||||||
initialText: block.content
|
initialText: block.content
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isStreaming = block.status === 'streaming'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newContent = block.content || ''
|
const newContent = block.content || ''
|
||||||
const oldContent = prevContentRef.current || ''
|
const oldContent = prevContentRef.current || ''
|
||||||
@@ -85,9 +88,8 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
|||||||
prevBlockIdRef.current = block.id
|
prevBlockIdRef.current = block.id
|
||||||
|
|
||||||
// 更新 stream 状态
|
// 更新 stream 状态
|
||||||
const isStreaming = block.status === 'streaming'
|
|
||||||
setIsStreamDone(!isStreaming)
|
setIsStreamDone(!isStreaming)
|
||||||
}, [block.content, block.id, block.status, addChunk, reset])
|
}, [block.content, block.id, block.status, addChunk, reset, isStreaming])
|
||||||
|
|
||||||
const remarkPlugins = useMemo(() => {
|
const remarkPlugins = useMemo(() => {
|
||||||
const plugins = [
|
const plugins = [
|
||||||
@@ -130,14 +132,16 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
|||||||
table: (props: any) => <Table {...props} blockId={block.id} />,
|
table: (props: any) => <Table {...props} blockId={block.id} />,
|
||||||
img: (props: any) => <ImageViewer style={{ maxWidth: 500, maxHeight: 500 }} {...props} />,
|
img: (props: any) => <ImageViewer style={{ maxWidth: 500, maxHeight: 500 }} {...props} />,
|
||||||
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />,
|
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />,
|
||||||
p: (props) => {
|
p: SmoothFade((props) => {
|
||||||
const hasImage = props?.node?.children?.some((child: any) => child.tagName === 'img')
|
const hasImage = props?.node?.children?.some((child: any) => child.tagName === 'img')
|
||||||
if (hasImage) return <div {...props} />
|
if (hasImage) return <div {...props} />
|
||||||
return <p {...props} />
|
return <p {...props} />
|
||||||
},
|
}, isStreaming),
|
||||||
svg: MarkdownSvgRenderer
|
svg: MarkdownSvgRenderer,
|
||||||
|
li: SmoothFade((props) => <li {...props} />, isStreaming),
|
||||||
|
span: SmoothFade((props) => <span {...props} />, isStreaming)
|
||||||
} as Partial<Components>
|
} as Partial<Components>
|
||||||
}, [block.id])
|
}, [block.id, isStreaming])
|
||||||
|
|
||||||
if (/<style\b[^>]*>/i.test(messageContent)) {
|
if (/<style\b[^>]*>/i.test(messageContent)) {
|
||||||
components.style = MarkdownShadowDOMRenderer as any
|
components.style = MarkdownShadowDOMRenderer as any
|
||||||
@@ -168,3 +172,23 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default memo(Markdown)
|
export default memo(Markdown)
|
||||||
|
|
||||||
|
const SmoothFade = (Comp: React.ElementType, isStreaming: boolean) => {
|
||||||
|
const handleAnimationEnd = (e: React.AnimationEvent) => {
|
||||||
|
// 动画结束后移除类名
|
||||||
|
if (e.animationName === 'fadeInWithBlur') {
|
||||||
|
e.currentTarget.classList.remove('animate-[fadeInWithBlur_500ms_ease-out_forwards]')
|
||||||
|
e.currentTarget.classList.remove('opacity-0')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ({ children, ...rest }) => {
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
{...rest}
|
||||||
|
className={isStreaming ? 'animate-[fadeInWithBlur_500ms_ease-out_forwards] opacity-0' : ''}
|
||||||
|
onAnimationEnd={handleAnimationEnd}>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +1,20 @@
|
|||||||
import { Spinner } from '@heroui/react'
|
import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
||||||
import { MessageBlockStatus, MessageBlockType, type PlaceholderMessageBlock } from '@renderer/types/newMessage'
|
import { Loader } from '@renderer/ui/loader'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
interface PlaceholderBlockProps {
|
interface PlaceholderBlockProps {
|
||||||
block: PlaceholderMessageBlock
|
status: MessageBlockStatus
|
||||||
|
type: MessageBlockType
|
||||||
}
|
}
|
||||||
const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ block }) => {
|
const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ status, type }) => {
|
||||||
if (block.status === MessageBlockStatus.PROCESSING && block.type === MessageBlockType.UNKNOWN) {
|
if (status === MessageBlockStatus.PROCESSING && type === MessageBlockType.UNKNOWN) {
|
||||||
return (
|
return (
|
||||||
<MessageContentLoading>
|
<div className="-mt-2">
|
||||||
<Spinner color="current" variant="dots" />
|
<Loader variant="terminal" />
|
||||||
</MessageContentLoading>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const MessageContentLoading = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
height: 32px;
|
|
||||||
margin-top: -5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
`
|
|
||||||
export default React.memo(PlaceholderBlock)
|
export default React.memo(PlaceholderBlock)
|
||||||
|
|||||||
@@ -215,15 +215,7 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
|
|||||||
})}
|
})}
|
||||||
{isProcessing && (
|
{isProcessing && (
|
||||||
<AnimatedBlockWrapper key="message-loading-placeholder" enableAnimation={true}>
|
<AnimatedBlockWrapper key="message-loading-placeholder" enableAnimation={true}>
|
||||||
<PlaceholderBlock
|
<PlaceholderBlock type={MessageBlockType.UNKNOWN} status={MessageBlockStatus.PROCESSING} />
|
||||||
block={{
|
|
||||||
id: `loading-${message.id}`,
|
|
||||||
messageId: message.id,
|
|
||||||
type: MessageBlockType.UNKNOWN,
|
|
||||||
status: MessageBlockStatus.PROCESSING,
|
|
||||||
createdAt: new Date().toISOString()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</AnimatedBlockWrapper>
|
</AnimatedBlockWrapper>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
374
src/renderer/src/ui/loader.tsx
Normal file
374
src/renderer/src/ui/loader.tsx
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
import { cn } from '@renderer/utils/index'
|
||||||
|
|
||||||
|
export interface LoaderProps {
|
||||||
|
variant?:
|
||||||
|
| 'circular'
|
||||||
|
| 'classic'
|
||||||
|
| 'pulse'
|
||||||
|
| 'pulse-dot'
|
||||||
|
| 'dots'
|
||||||
|
| 'typing'
|
||||||
|
| 'wave'
|
||||||
|
| 'bars'
|
||||||
|
| 'terminal'
|
||||||
|
| 'text-blink'
|
||||||
|
| 'text-shimmer'
|
||||||
|
| 'loading-dots'
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
|
text?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CircularLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'size-4',
|
||||||
|
md: 'size-5',
|
||||||
|
lg: 'size-6'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'animate-spin rounded-full border-2 border-primary border-t-transparent',
|
||||||
|
sizeClasses[size],
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
<span className="sr-only">Loading</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClassicLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'size-4',
|
||||||
|
md: 'size-5',
|
||||||
|
lg: 'size-6'
|
||||||
|
}
|
||||||
|
|
||||||
|
const barSizes = {
|
||||||
|
sm: { height: '6px', width: '1.5px' },
|
||||||
|
md: { height: '8px', width: '2px' },
|
||||||
|
lg: { height: '10px', width: '2.5px' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('relative', sizeClasses[size], className)}>
|
||||||
|
<div className="absolute h-full w-full">
|
||||||
|
{[...Array(12)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="absolute animate-[spinner-fade_1.2s_linear_infinite] rounded-full bg-primary"
|
||||||
|
style={{
|
||||||
|
top: '0',
|
||||||
|
left: '50%',
|
||||||
|
marginLeft: size === 'sm' ? '-0.75px' : size === 'lg' ? '-1.25px' : '-1px',
|
||||||
|
transformOrigin: `${size === 'sm' ? '0.75px' : size === 'lg' ? '1.25px' : '1px'} ${size === 'sm' ? '10px' : size === 'lg' ? '14px' : '12px'}`,
|
||||||
|
transform: `rotate(${i * 30}deg)`,
|
||||||
|
opacity: 0,
|
||||||
|
animationDelay: `${i * 0.1}s`,
|
||||||
|
height: barSizes[size].height,
|
||||||
|
width: barSizes[size].width
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="sr-only">Loading</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PulseLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'size-4',
|
||||||
|
md: 'size-5',
|
||||||
|
lg: 'size-6'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('relative', sizeClasses[size], className)}>
|
||||||
|
<div className="absolute inset-0 animate-[thin-pulse_1.5s_ease-in-out_infinite] rounded-full border-2 border-primary" />
|
||||||
|
<span className="sr-only">Loading</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PulseDotLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'size-1',
|
||||||
|
md: 'size-2',
|
||||||
|
lg: 'size-3'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'animate-[pulse-dot_1.2s_ease-in-out_infinite] rounded-full bg-primary',
|
||||||
|
sizeClasses[size],
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
<span className="sr-only">Loading</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DotsLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||||
|
const dotSizes = {
|
||||||
|
sm: 'h-1.5 w-1.5',
|
||||||
|
md: 'h-2 w-2',
|
||||||
|
lg: 'h-2.5 w-2.5'
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerSizes = {
|
||||||
|
sm: 'h-4',
|
||||||
|
md: 'h-5',
|
||||||
|
lg: 'h-6'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center space-x-1', containerSizes[size], className)}>
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn('animate-[bounce-dots_1.4s_ease-in-out_infinite] rounded-full bg-primary', dotSizes[size])}
|
||||||
|
style={{
|
||||||
|
animationDelay: `${i * 160}ms`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<span className="sr-only">Loading</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TypingLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||||
|
const dotSizes = {
|
||||||
|
sm: 'h-1 w-1',
|
||||||
|
md: 'h-1.5 w-1.5',
|
||||||
|
lg: 'h-2 w-2'
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerSizes = {
|
||||||
|
sm: 'h-4',
|
||||||
|
md: 'h-5',
|
||||||
|
lg: 'h-6'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center space-x-1', containerSizes[size], className)}>
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn('animate-[typing_1s_infinite] rounded-full bg-primary', dotSizes[size])}
|
||||||
|
style={{
|
||||||
|
animationDelay: `${i * 250}ms`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<span className="sr-only">Loading</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WaveLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||||
|
const barWidths = {
|
||||||
|
sm: 'w-0.5',
|
||||||
|
md: 'w-0.5',
|
||||||
|
lg: 'w-1'
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerSizes = {
|
||||||
|
sm: 'h-4',
|
||||||
|
md: 'h-5',
|
||||||
|
lg: 'h-6'
|
||||||
|
}
|
||||||
|
|
||||||
|
const heights = {
|
||||||
|
sm: ['6px', '9px', '12px', '9px', '6px'],
|
||||||
|
md: ['8px', '12px', '16px', '12px', '8px'],
|
||||||
|
lg: ['10px', '15px', '20px', '15px', '10px']
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center gap-0.5', containerSizes[size], className)}>
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn('animate-[wave_1s_ease-in-out_infinite] rounded-full bg-primary', barWidths[size])}
|
||||||
|
style={{
|
||||||
|
animationDelay: `${i * 100}ms`,
|
||||||
|
height: heights[size][i]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<span className="sr-only">Loading</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BarsLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||||
|
const barWidths = {
|
||||||
|
sm: 'w-1',
|
||||||
|
md: 'w-1.5',
|
||||||
|
lg: 'w-2'
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerSizes = {
|
||||||
|
sm: 'h-4 gap-1',
|
||||||
|
md: 'h-5 gap-1.5',
|
||||||
|
lg: 'h-6 gap-2'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex', containerSizes[size], className)}>
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn('h-full animate-[wave-bars_1.2s_ease-in-out_infinite] bg-primary', barWidths[size])}
|
||||||
|
style={{
|
||||||
|
animationDelay: `${i * 0.2}s`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<span className="sr-only">Loading</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TerminalLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||||
|
const cursorSizes = {
|
||||||
|
sm: 'h-3 w-1.5',
|
||||||
|
md: 'h-4 w-2',
|
||||||
|
lg: 'h-5 w-2.5'
|
||||||
|
}
|
||||||
|
|
||||||
|
const textSizes = {
|
||||||
|
sm: 'text-xs',
|
||||||
|
md: 'text-sm',
|
||||||
|
lg: 'text-base'
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerSizes = {
|
||||||
|
sm: 'h-4',
|
||||||
|
md: 'h-5',
|
||||||
|
lg: 'h-6'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center space-x-1', containerSizes[size], className)}>
|
||||||
|
<span className={cn('font-mono text-primary', textSizes[size])}>{'>'}</span>
|
||||||
|
<div className={cn('animate-[blink_1s_step-end_infinite] bg-primary', cursorSizes[size])} />
|
||||||
|
<span className="sr-only">Loading</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextBlinkLoader({
|
||||||
|
text = 'Thinking',
|
||||||
|
className,
|
||||||
|
size = 'md'
|
||||||
|
}: {
|
||||||
|
text?: string
|
||||||
|
className?: string
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
|
}) {
|
||||||
|
const textSizes = {
|
||||||
|
sm: 'text-xs',
|
||||||
|
md: 'text-sm',
|
||||||
|
lg: 'text-base'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('animate-[text-blink_2s_ease-in-out_infinite] font-medium', textSizes[size], className)}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextShimmerLoader({
|
||||||
|
text = 'Thinking',
|
||||||
|
className,
|
||||||
|
size = 'md'
|
||||||
|
}: {
|
||||||
|
text?: string
|
||||||
|
className?: string
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
|
}) {
|
||||||
|
const textSizes = {
|
||||||
|
sm: 'text-xs',
|
||||||
|
md: 'text-sm',
|
||||||
|
lg: 'text-base'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'bg-[linear-gradient(to_right,var(--muted-foreground)_40%,var(--foreground)_60%,var(--muted-foreground)_80%)]',
|
||||||
|
'bg-[200%_auto] bg-clip-text font-medium text-transparent',
|
||||||
|
'animate-[shimmer_4s_infinite_linear]',
|
||||||
|
textSizes[size],
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextDotsLoader({
|
||||||
|
className,
|
||||||
|
text = 'Thinking',
|
||||||
|
size = 'md'
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
text?: string
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
|
}) {
|
||||||
|
const textSizes = {
|
||||||
|
sm: 'text-xs',
|
||||||
|
md: 'text-sm',
|
||||||
|
lg: 'text-base'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('inline-flex items-center', className)}>
|
||||||
|
<span className={cn('font-medium text-primary', textSizes[size])}>{text}</span>
|
||||||
|
<span className="inline-flex">
|
||||||
|
<span className="animate-[loading-dots_1.4s_infinite_0.2s] text-primary">.</span>
|
||||||
|
<span className="animate-[loading-dots_1.4s_infinite_0.4s] text-primary">.</span>
|
||||||
|
<span className="animate-[loading-dots_1.4s_infinite_0.6s] text-primary">.</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Loader({ variant = 'circular', size = 'md', text, className }: LoaderProps) {
|
||||||
|
switch (variant) {
|
||||||
|
case 'circular':
|
||||||
|
return <CircularLoader size={size} className={className} />
|
||||||
|
case 'classic':
|
||||||
|
return <ClassicLoader size={size} className={className} />
|
||||||
|
case 'pulse':
|
||||||
|
return <PulseLoader size={size} className={className} />
|
||||||
|
case 'pulse-dot':
|
||||||
|
return <PulseDotLoader size={size} className={className} />
|
||||||
|
case 'dots':
|
||||||
|
return <DotsLoader size={size} className={className} />
|
||||||
|
case 'typing':
|
||||||
|
return <TypingLoader size={size} className={className} />
|
||||||
|
case 'wave':
|
||||||
|
return <WaveLoader size={size} className={className} />
|
||||||
|
case 'bars':
|
||||||
|
return <BarsLoader size={size} className={className} />
|
||||||
|
case 'terminal':
|
||||||
|
return <TerminalLoader size={size} className={className} />
|
||||||
|
case 'text-blink':
|
||||||
|
return <TextBlinkLoader text={text} size={size} className={className} />
|
||||||
|
case 'text-shimmer':
|
||||||
|
return <TextShimmerLoader text={text} size={size} className={className} />
|
||||||
|
case 'loading-dots':
|
||||||
|
return <TextDotsLoader text={text} size={size} className={className} />
|
||||||
|
default:
|
||||||
|
return <CircularLoader size={size} className={className} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Loader }
|
||||||
@@ -234,3 +234,4 @@ export * from './match'
|
|||||||
export * from './naming'
|
export * from './naming'
|
||||||
export * from './sort'
|
export * from './sort'
|
||||||
export * from './style'
|
export * from './style'
|
||||||
|
export { cn } from '@heroui/react'
|
||||||
|
|||||||
Reference in New Issue
Block a user