feat(ui): add new spinner component and update lucide-react

- Add new general-purpose spinner component with multiple variants
- Rename old Spinner component to SearchSpinner to clarify its purpose
- Update lucide-react dependency to v0.546.0
This commit is contained in:
icarus 2025-10-22 05:26:00 +08:00
parent a0445a307a
commit 578cf38072
5 changed files with 229 additions and 12 deletions

View File

@ -291,7 +291,7 @@
"lint-staged": "^15.5.0",
"lodash": "^4.17.21",
"lru-cache": "^11.1.0",
"lucide-react": "^0.525.0",
"lucide-react": "^0.546.0",
"macos-release": "^3.4.0",
"markdown-it": "^14.1.0",
"mermaid": "^11.10.1",

View File

@ -17,6 +17,7 @@ const spinnerVariants = {
}
}
// FIXME: This is not a general spinner. It's just for searching.
export default function Spinner({ text, className = '' }: Props) {
return (
<motion.div

View File

@ -9,7 +9,7 @@ export { default as EmojiIcon } from './base/EmojiIcon'
export type { CustomFallbackProps, ErrorBoundaryCustomizedProps } from './base/ErrorBoundary'
export { ErrorBoundary } from './base/ErrorBoundary'
export { default as IndicatorLight } from './base/IndicatorLight'
export { default as Spinner } from './base/Spinner'
export { default as SearchSpinner } from './base/Spinner'
export type { StatusTagProps, StatusType } from './base/StatusTag'
export { ErrorTag, InfoTag, StatusTag, SuccessTag, WarnTag } from './base/StatusTag'
export { DescriptionSwitch, Switch } from './base/Switch'
@ -97,3 +97,4 @@ export * from './ui/command'
export * from './ui/dialog'
export * from './ui/popover'
export * from './ui/shadcn-io/dropzone'
export * from './ui/shadcn-io/spinner'

View File

@ -0,0 +1,215 @@
import { cn } from '@cherrystudio/ui/utils/index'
import { LoaderCircleIcon, LoaderIcon, LoaderPinwheelIcon, type LucideProps } from 'lucide-react'
type SpinnerVariantProps = Omit<SpinnerProps, 'variant'>
const Default = ({ className, ...props }: SpinnerVariantProps) => (
<LoaderIcon className={cn('animate-spin', className)} {...props} />
)
const Circle = ({ className, ...props }: SpinnerVariantProps) => (
<LoaderCircleIcon className={cn('animate-spin', className)} {...props} />
)
const Pinwheel = ({ className, ...props }: SpinnerVariantProps) => (
<LoaderPinwheelIcon className={cn('animate-spin', className)} {...props} />
)
const CircleFilled = ({ className, size = 24, ...props }: SpinnerVariantProps) => (
<div className="relative" style={{ width: size, height: size }}>
<div className="absolute inset-0 rotate-180">
<LoaderCircleIcon
className={cn('animate-spin', className, 'text-foreground opacity-20')}
size={size}
{...props}
/>
</div>
<LoaderCircleIcon className={cn('relative animate-spin', className)} size={size} {...props} />
</div>
)
const Ellipsis = ({ size = 24, ...props }: SpinnerVariantProps) => {
return (
<svg height={size} viewBox="0 0 24 24" width={size} xmlns="http://www.w3.org/2000/svg" {...props}>
<title>Loading...</title>
<circle cx="4" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="0;ellipsis3.end+0.25s"
calcMode="spline"
dur="0.6s"
id="ellipsis1"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
<circle cx="12" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="ellipsis1.begin+0.1s"
calcMode="spline"
dur="0.6s"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
<circle cx="20" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="ellipsis1.begin+0.2s"
calcMode="spline"
dur="0.6s"
id="ellipsis3"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
</svg>
)
}
const Ring = ({ size = 24, ...props }: SpinnerVariantProps) => (
<svg
height={size}
stroke="currentColor"
viewBox="0 0 44 44"
width={size}
xmlns="http://www.w3.org/2000/svg"
{...props}>
<title>Loading...</title>
<g fill="none" fillRule="evenodd" strokeWidth="2">
<circle cx="22" cy="22" r="1">
<animate
attributeName="r"
begin="0s"
calcMode="spline"
dur="1.8s"
keySplines="0.165, 0.84, 0.44, 1"
keyTimes="0; 1"
repeatCount="indefinite"
values="1; 20"
/>
<animate
attributeName="stroke-opacity"
begin="0s"
calcMode="spline"
dur="1.8s"
keySplines="0.3, 0.61, 0.355, 1"
keyTimes="0; 1"
repeatCount="indefinite"
values="1; 0"
/>
</circle>
<circle cx="22" cy="22" r="1">
<animate
attributeName="r"
begin="-0.9s"
calcMode="spline"
dur="1.8s"
keySplines="0.165, 0.84, 0.44, 1"
keyTimes="0; 1"
repeatCount="indefinite"
values="1; 20"
/>
<animate
attributeName="stroke-opacity"
begin="-0.9s"
calcMode="spline"
dur="1.8s"
keySplines="0.3, 0.61, 0.355, 1"
keyTimes="0; 1"
repeatCount="indefinite"
values="1; 0"
/>
</circle>
</g>
</svg>
)
const Bars = ({ size = 24, ...props }: SpinnerVariantProps) => (
<svg height={size} viewBox="0 0 24 24" width={size} xmlns="http://www.w3.org/2000/svg" {...props}>
<title>Loading...</title>
<style>{`
.spinner-bar {
animation: spinner-bars-animation .8s linear infinite;
animation-delay: -.8s;
}
.spinner-bars-2 {
animation-delay: -.65s;
}
.spinner-bars-3 {
animation-delay: -0.5s;
}
@keyframes spinner-bars-animation {
0% {
y: 1px;
height: 22px;
}
93.75% {
y: 5px;
height: 14px;
opacity: 0.2;
}
}
`}</style>
<rect className="spinner-bar" fill="currentColor" height="22" width="6" x="1" y="1" />
<rect className="spinner-bar spinner-bars-2" fill="currentColor" height="22" width="6" x="9" y="1" />
<rect className="spinner-bar spinner-bars-3" fill="currentColor" height="22" width="6" x="17" y="1" />
</svg>
)
const Infinite = ({ size = 24, ...props }: SpinnerVariantProps) => (
<svg
height={size}
preserveAspectRatio="xMidYMid"
viewBox="0 0 100 100"
width={size}
xmlns="http://www.w3.org/2000/svg"
{...props}>
<title>Loading...</title>
<path
d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z"
fill="none"
stroke="currentColor"
strokeDasharray="205.271142578125 51.317785644531256"
strokeLinecap="round"
strokeWidth="10"
style={{
transform: 'scale(0.8)',
transformOrigin: '50px 50px'
}}>
<animate
attributeName="stroke-dashoffset"
dur="2s"
keyTimes="0;1"
repeatCount="indefinite"
values="0;256.58892822265625"
/>
</path>
</svg>
)
export type SpinnerProps = LucideProps & {
variant?: 'default' | 'circle' | 'pinwheel' | 'circle-filled' | 'ellipsis' | 'ring' | 'bars' | 'infinite'
}
export const Spinner = ({ variant, ...props }: SpinnerProps) => {
switch (variant) {
case 'circle':
return <Circle {...props} />
case 'pinwheel':
return <Pinwheel {...props} />
case 'circle-filled':
return <CircleFilled {...props} />
case 'ellipsis':
return <Ellipsis {...props} />
case 'ring':
return <Ring {...props} />
case 'bars':
return <Bars {...props} />
case 'infinite':
return <Infinite {...props} />
default:
return <Default {...props} />
}
}

View File

@ -17318,7 +17318,7 @@ __metadata:
lint-staged: "npm:^15.5.0"
lodash: "npm:^4.17.21"
lru-cache: "npm:^11.1.0"
lucide-react: "npm:^0.525.0"
lucide-react: "npm:^0.546.0"
macos-release: "npm:^3.4.0"
markdown-it: "npm:^14.1.0"
mermaid: "npm:^11.10.1"
@ -25322,15 +25322,6 @@ __metadata:
languageName: node
linkType: hard
"lucide-react@npm:^0.525.0":
version: 0.525.0
resolution: "lucide-react@npm:0.525.0"
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/81c4438e2cf1c86ea2ebe0a97b378201512450894283cccce766a89bc6a4e47c8df1d9115d845d98f582bb0a10be31c454aa232520fea35018dac1cd8466ea9b
languageName: node
linkType: hard
"lucide-react@npm:^0.545.0":
version: 0.545.0
resolution: "lucide-react@npm:0.545.0"
@ -25340,6 +25331,15 @@ __metadata:
languageName: node
linkType: hard
"lucide-react@npm:^0.546.0":
version: 0.546.0
resolution: "lucide-react@npm:0.546.0"
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/42ee0bd358517f012297aefb69b54da0b3c62f1ac8485ffa24141b63f05c9f4a682eacc2637c4b13f597aed11f9a8c79627af0c17717400bebb25581daeaad80
languageName: node
linkType: hard
"lz-string@npm:^1.5.0":
version: 1.5.0
resolution: "lz-string@npm:1.5.0"