diff --git a/apps/docs/features/ui/LoadingBeam.tsx b/apps/docs/features/ui/LoadingBeam.tsx new file mode 100644 index 00000000000..e9dd73faf67 --- /dev/null +++ b/apps/docs/features/ui/LoadingBeam.tsx @@ -0,0 +1,33 @@ +'use client' + +import { useState } from 'react' +import { cn } from 'ui' + +export interface LoadingBeamProps { + isActive: boolean + direction?: 'forward' | 'backward' + className?: string +} + +export const LoadingBeam = ({ isActive, direction = 'forward', className }: LoadingBeamProps) => { + const [run, setRun] = useState(0) + const [runDirection, setRunDirection] = useState(direction) + const [wasActive, setWasActive] = useState(isActive) + if (isActive !== wasActive) { + setWasActive(isActive) + if (isActive) { + setRun((current) => current + 1) + setRunDirection(direction) + } + } + + return ( +
+ ) +} diff --git a/apps/docs/styles/globals.css b/apps/docs/styles/globals.css index 2a39bcb51c0..7cf44396ac4 100644 --- a/apps/docs/styles/globals.css +++ b/apps/docs/styles/globals.css @@ -1,6 +1,7 @@ @import 'config/tailwind.config.css'; @import './code-block.css'; @import './reference.css'; +@import './loading-beam.css'; @source '../app/**/*.{ts,tsx,mdx}'; @source '../components/**/*.tsx'; diff --git a/apps/docs/styles/loading-beam.css b/apps/docs/styles/loading-beam.css new file mode 100644 index 00000000000..da770e12e04 --- /dev/null +++ b/apps/docs/styles/loading-beam.css @@ -0,0 +1,76 @@ +@property --loading-beam-x { + syntax: ''; + initial-value: 0.5; + inherits: true; +} + +@keyframes loading-beam-sweep { + 0%, + 100% { + --loading-beam-x: 0; + } + 50% { + --loading-beam-x: 1; + } +} + +@layer components { + .loading-beam { + --loading-beam-color: var(--foreground-default); + --loading-beam-half: 100px; + --loading-beam-center: calc( + var(--loading-beam-half) + var(--loading-beam-x) * (100% - var(--loading-beam-half) * 2) + ); + position: absolute; + inset: auto 0 0; + height: 16px; + overflow: hidden; + pointer-events: none; + opacity: 0; + transition: opacity 150ms ease-out; + animation: loading-beam-sweep 2s ease-in-out -0.25s infinite paused; + } + + .loading-beam[data-direction='backward'] { + animation-delay: -1.25s; + } + + .loading-beam[data-active] { + opacity: 1; + transition-duration: 250ms; + animation-play-state: running; + } + + .loading-beam::before, + .loading-beam::after { + content: ''; + position: absolute; + } + + .loading-beam::before { + inset: 0 0 -8px; + filter: blur(4px); + background: radial-gradient( + 120px 12px at var(--loading-beam-center) calc(100% - 8px), + color-mix(in oklch, var(--loading-beam-color) 3.2%, transparent), + transparent + ); + } + + .loading-beam::after { + inset: auto 0 0; + height: 1px; + background: radial-gradient( + var(--loading-beam-half) 4px at var(--loading-beam-center) 100%, + color-mix(in oklch, var(--loading-beam-color) 44.5%, transparent), + color-mix(in oklch, var(--loading-beam-color) 16%, transparent) 45%, + transparent + ); + } + + @media (prefers-reduced-motion: reduce) { + .loading-beam { + animation: none; + } + } +}