feat(ui): add Spinner component from multica

Port the SpinKit Grid 3x3 spinner as a shared UI component.
Uses currentColor and em sizing for flexible theming and scaling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-01-30 21:01:48 +08:00
parent 01790a57d2
commit f348d91c18
2 changed files with 77 additions and 0 deletions

View file

@ -0,0 +1,36 @@
/**
* Spinner - 3x3 grid spinner based on SpinKit Grid
*
* Features:
* - Uses currentColor (inherits text color from parent, typically theme primary)
* - Uses em sizing (scales with font-size)
* - 3x3 grid of cubes with staggered scale animation
* - Pure CSS animation (no JS state)
*
* Usage:
* <Spinner className="text-primary" /> // Uses primary theme color
* <Spinner className="text-muted-foreground" /> // Uses muted color
* <Spinner className="text-xs" /> // Controls size via Tailwind font-size
*/
import { cn } from "@multica/ui/lib/utils"
export interface SpinnerProps {
/** Additional className for styling (color via text-*, size via Tailwind text-*) */
className?: string
}
export function Spinner({ className }: SpinnerProps) {
return (
<span className={cn("spinner", className)} role="status" aria-label="Loading">
<span className="spinner-cube" />
<span className="spinner-cube" />
<span className="spinner-cube" />
<span className="spinner-cube" />
<span className="spinner-cube" />
<span className="spinner-cube" />
<span className="spinner-cube" />
<span className="spinner-cube" />
<span className="spinner-cube" />
</span>
)
}

View file

@ -141,3 +141,44 @@
@apply bg-background text-foreground;
}
}
/* SPINNER - SpinKit Grid (3x3) */
.spinner {
display: inline-grid;
grid-template-columns: repeat(3, 1fr);
width: 1em;
height: 1em;
gap: 0.08em;
}
.spinner-cube {
background-color: currentColor;
animation: spinner-grid 1.3s infinite ease-in-out;
transform: scale3d(0.5, 0.5, 1);
}
.spinner-cube:nth-child(1) { animation-delay: 0.2s; }
.spinner-cube:nth-child(2) { animation-delay: 0.3s; }
.spinner-cube:nth-child(3) { animation-delay: 0.4s; }
.spinner-cube:nth-child(4) { animation-delay: 0.1s; }
.spinner-cube:nth-child(5) { animation-delay: 0.2s; }
.spinner-cube:nth-child(6) { animation-delay: 0.3s; }
.spinner-cube:nth-child(7) { animation-delay: 0s; }
.spinner-cube:nth-child(8) { animation-delay: 0.1s; }
.spinner-cube:nth-child(9) { animation-delay: 0.2s; }
@keyframes spinner-grid {
0%, 70%, 100% {
transform: scale3d(0.5, 0.5, 1);
}
35% {
transform: scale3d(0, 0, 1);
}
}
@media (prefers-reduced-motion: reduce) {
.spinner-cube {
animation: none;
opacity: 0.7;
}
}