diff --git a/desktop/src/shared/styles/globals.css b/desktop/src/shared/styles/globals.css index bb3fdb249..3cb2da523 100644 --- a/desktop/src/shared/styles/globals.css +++ b/desktop/src/shared/styles/globals.css @@ -2,6 +2,16 @@ @import "tw-animate-css"; @config "../../../tailwind.config.js"; +.sprout-arc-spinner { + animation: sprout-arc-spinner-spin 500ms linear infinite; +} + +@keyframes sprout-arc-spinner-spin { + to { + transform: rotate(360deg); + } +} + .buzz-emoji-mart { align-items: stretch; display: flex; @@ -837,6 +847,121 @@ } } +:root { + --skel-pulse-dur: 1000ms; + --skel-pulse-count: infinite; + --skel-pulse-min: 0.5; + --skel-reveal-dur: 400ms; + --skel-reveal-blur: 2px; + --skel-reveal-ease: ease-in-out; +} + +.t-skel { + position: relative; +} + +.t-skel[data-layout="flow"] { + display: grid; +} + +.t-skel-skeleton, +.t-skel-content { + inset: 0; + position: absolute; +} + +.t-skel[data-layout="flow"] > .t-skel-skeleton, +.t-skel[data-layout="flow"] > .t-skel-content { + grid-area: 1 / 1; + width: 100%; +} + +.t-skel[data-layout="flow"] > .t-skel-skeleton { + inset: auto; + position: relative; +} + +.t-skel[data-layout="flow"] > .t-skel-content, +.t-skel[data-layout="flow"].is-revealed > .t-skel-skeleton { + inset: 0; + position: absolute; +} + +.t-skel[data-layout="flow"].is-revealed > .t-skel-content { + inset: auto; + position: relative; +} + +.t-skel-skeleton { + filter: blur(0); + opacity: 1; + transition: + opacity var(--skel-reveal-dur) var(--skel-reveal-ease), + filter var(--skel-reveal-dur) var(--skel-reveal-ease); + z-index: 1; +} + +.t-skel-content { + filter: blur(var(--skel-reveal-blur)); + opacity: 0; + pointer-events: none; + transition: + opacity var(--skel-reveal-dur) var(--skel-reveal-ease), + filter var(--skel-reveal-dur) var(--skel-reveal-ease); + z-index: 2; +} + +.t-skel.is-revealed .t-skel-skeleton { + filter: blur(var(--skel-reveal-blur)); + opacity: 0; + pointer-events: none; +} + +.t-skel.is-revealed .t-skel-content { + filter: blur(0); + opacity: 1; + pointer-events: auto; +} + +.t-skel.is-resetting .t-skel-skeleton, +.t-skel.is-resetting .t-skel-content { + transition: none; +} + +.t-skel-bar.is-pulsing, +.t-skel-skeleton.is-pulsing > :not(:has(.t-skel-bar)) { + animation: t-skel-pulse var(--skel-pulse-dur) ease-in-out + var(--skel-pulse-count); +} + +.t-skel.is-revealed .t-skel-bar.is-pulsing, +.t-skel.is-revealed .t-skel-skeleton.is-pulsing > :not(:has(.t-skel-bar)) { + animation: none; +} + +@keyframes t-skel-pulse { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: var(--skel-pulse-min); + } +} + +@media (prefers-reduced-motion: reduce) { + .t-skel-skeleton, + .t-skel-content { + transition: none; + } + + .t-skel-bar.is-pulsing, + .t-skel-skeleton.is-pulsing > :not(:has(.t-skel-bar)) { + animation: none; + } +} + @layer components { .buzz-startup-shell { min-height: 100dvh; diff --git a/desktop/src/shared/ui/skeleton.tsx b/desktop/src/shared/ui/skeleton.tsx index 563cc4c9e..b324eb5da 100644 --- a/desktop/src/shared/ui/skeleton.tsx +++ b/desktop/src/shared/ui/skeleton.tsx @@ -1,15 +1,99 @@ +import * as React from "react"; + import { cn } from "@/shared/lib/cn"; -function Skeleton({ +type SkeletonProps = React.HTMLAttributes & { + pulsing?: boolean; +}; + +function Skeleton({ className, pulsing = true, ...props }: SkeletonProps) { + return ( +