From f348d91c180ec9475b69f27a1e1e44347183cd07 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:01:48 +0800 Subject: [PATCH] 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 --- packages/ui/src/components/spinner.tsx | 36 ++++++++++++++++++++++ packages/ui/src/styles/globals.css | 41 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 packages/ui/src/components/spinner.tsx diff --git a/packages/ui/src/components/spinner.tsx b/packages/ui/src/components/spinner.tsx new file mode 100644 index 00000000..10d802ab --- /dev/null +++ b/packages/ui/src/components/spinner.tsx @@ -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: + * // Uses primary theme color + * // Uses muted color + * // 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 ( + + + + + + + + + + + + ) +} diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css index 3afc7312..dd74c884 100644 --- a/packages/ui/src/styles/globals.css +++ b/packages/ui/src/styles/globals.css @@ -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; + } +}