Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/docs/src/components/landing/animations/alert-bell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Bell } from 'lucide-react'

export function AlertBellAnim() {
const channels = ['Webhook', 'Telegram', 'Bark', 'Email', 'APNs']
return (
<div
aria-label="Animated demo of multi-channel alerts"
className="flex h-full flex-col items-center justify-center gap-3"
role="img"
>
<div className="relative">
<Bell className="bell-shake h-9 w-9 text-amber-300" />
<span className="absolute -top-0.5 -right-0.5 h-2.5 w-2.5 rounded-full bg-red-500 ring-2 ring-zinc-950" />
</div>
<div className="flex flex-wrap justify-center gap-1.5">
{channels.map((c, i) => (
<span
className="fade-cycle rounded-full border border-white/10 bg-white/[0.04] px-2 py-0.5 font-mono text-[10px] text-zinc-300"
key={c}
style={{ animationDelay: `${i * 0.3}s` }}
>
{c}
</span>
))}
</div>
</div>
)
}
20 changes: 20 additions & 0 deletions apps/docs/src/components/landing/animations/color-ring.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function ColorRingAnim() {
const stops = ['#ffb300', '#4cc9f0', '#22c55e', '#a855f7', '#ef4444', '#ffb300']
const gradient = stops.map((c, i) => `${c} ${(i / (stops.length - 1)) * 360}deg`).join(', ')
return (
<div
aria-label="Animated demo of theme customization"
className="flex h-full items-center justify-center"
role="img"
>
<div className="relative">
<div className="orbit-anim h-28 w-28 rounded-full" style={{ background: `conic-gradient(${gradient})` }} />
<div className="absolute inset-2 rounded-full bg-zinc-950" />
<div className="fade-cycle absolute inset-4 rounded-full bg-amber-400 shadow-[0_0_30px_-6px_rgba(255,179,0,0.7)]" />
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2 rounded-full bg-zinc-900/80 px-2 py-0.5 font-mono text-[10px] text-amber-300">
OKLCH
</div>
</div>
</div>
)
}
40 changes: 40 additions & 0 deletions apps/docs/src/components/landing/animations/data-stream.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export function DataStreamAnim() {
return (
<div
aria-label="Animated demo of real-time WebSocket streaming"
className="relative flex h-40 items-center justify-between px-6"
role="img"
>
<Endpoint color="#ffb300" label="Server" />
<div className="relative mx-3 h-px flex-1 bg-gradient-to-r from-amber-400/30 via-cyan-300/30 to-amber-400/30">
<Particle colorClass="bg-amber-300" delay="0s" />
<Particle colorClass="bg-cyan-300" delay="0.6s" reverse />
<Particle colorClass="bg-amber-300" delay="1.2s" />
<Particle colorClass="bg-cyan-300" delay="1.8s" reverse />
</div>
<Endpoint color="#4cc9f0" label="Agent" />
</div>
)
}

function Endpoint({ label, color }: { label: string; color: string }) {
return (
<div className="flex flex-col items-center gap-1">
<div
className="h-10 w-10 rounded-lg border border-white/10 bg-white/[0.04]"
style={{ boxShadow: `0 0 24px -6px ${color}` }}
/>
<span className="font-mono text-[10px] text-zinc-400 uppercase tracking-wider">{label}</span>
</div>
)
}

function Particle({ delay, colorClass, reverse }: { delay: string; colorClass: string; reverse?: boolean }) {
return (
<span
aria-hidden
className={`stream-particle absolute top-1/2 h-1.5 w-3 -translate-y-1/2 rounded-full ${colorClass}`}
style={{ animationDelay: delay, animationDirection: reverse ? 'reverse' : 'normal' }}
/>
)
}
34 changes: 34 additions & 0 deletions apps/docs/src/components/landing/animations/docker-stack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Box } from 'lucide-react'

export function DockerStackAnim() {
const containers = [
{ name: 'web', tag: 'caddy:2', cpu: '0.4%', delay: '0s' },
{ name: 'api', tag: 'rust:1.84', cpu: '1.2%', delay: '0.4s' },
{ name: 'cache', tag: 'redis:7', cpu: '0.1%', delay: '0.8s' }
]
return (
<div
aria-label="Animated demo of Docker container management"
className="flex h-full flex-col justify-center gap-1.5"
role="img"
>
{containers.map((c) => (
<div
className="flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-2.5 py-1.5 font-mono text-[11px]"
key={c.name}
>
<Box className="h-3.5 w-3.5 shrink-0 text-cyan-300" />
<span className="truncate text-zinc-200">{c.name}</span>
<span className="truncate text-[10px] text-zinc-500">{c.tag}</span>
<span className="ml-auto inline-flex items-center gap-1.5 text-[10px] text-emerald-300">
<span
className="pulse-dot inline-block h-1.5 w-1.5 rounded-full bg-emerald-400"
style={{ animationDelay: c.delay }}
/>
{c.cpu}
</span>
</div>
))}
</div>
)
}
55 changes: 55 additions & 0 deletions apps/docs/src/components/landing/animations/file-tree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { File, FolderOpen, FolderTree, Upload } from 'lucide-react'
import type { ComponentType } from 'react'

export function FileTreeAnim() {
return (
<div
aria-label="Animated demo of the file manager"
className="flex h-full flex-col gap-2 font-mono text-xs"
role="img"
>
<div className="min-h-0 flex-1 space-y-0.5 overflow-hidden rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-zinc-300">
<Row Icon={FolderTree} label="/var/log" />
<Row Icon={FolderOpen} indent label="nginx" />
<Row Icon={File} indent2 label="access.log" muted="2.4 MB" />
<Row Icon={File} indent2 label="error.log" muted="312 KB" />
</div>
<div className="flex shrink-0 items-center gap-2.5">
<Upload aria-hidden className="h-3 w-3 shrink-0 text-amber-300" />
<span className="shrink-0 text-[10px] text-zinc-400">access.log → uploading</span>
<div className="relative h-1.5 flex-1 overflow-hidden rounded-full bg-white/5">
<span className="light-band absolute inset-y-0 left-0 w-1/2 bg-gradient-to-r from-amber-400 via-amber-300 to-amber-400" />
</div>
<span className="shrink-0 font-mono text-[10px] text-amber-300">64%</span>
</div>
</div>
)
}

function Row({
Icon,
label,
indent,
indent2,
muted
}: {
Icon: ComponentType<{ className?: string }>
label: string
indent?: boolean
indent2?: boolean
muted?: string
}) {
let pad = ''
if (indent2) {
pad = 'pl-6'
} else if (indent) {
pad = 'pl-3'
}
return (
<div className={`flex items-center gap-2 ${pad}`}>
<Icon className="h-3.5 w-3.5 text-amber-300" />
<span className="text-zinc-300">{label}</span>
{muted ? <span className="ml-auto text-[10px] text-zinc-500">{muted}</span> : null}
</div>
)
}
28 changes: 28 additions & 0 deletions apps/docs/src/components/landing/animations/install-binary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export function InstallBinaryAnim() {
return (
<div
aria-label="Animated demo of installing the ServerBee binary"
className="flex h-40 items-center justify-center"
role="img"
>
<div className="relative flex flex-col items-center gap-3">
<div className="rounded-lg border border-amber-400/40 bg-amber-400/10 px-4 py-2 font-mono text-amber-300 text-xs shadow-[0_0_30px_-12px_rgba(255,179,0,0.6)]">
serverbee
</div>
<svg aria-hidden="true" focusable="false" height="32" viewBox="0 0 20 32" width="20">
<path
d="M10 2 L10 24 M4 18 L10 26 L16 18"
fill="none"
stroke="#ffb300"
strokeLinecap="round"
strokeWidth="2"
/>
</svg>
<div className="flex items-center gap-2 rounded-md border border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-xs text-zinc-300">
<span className="pulse-dot inline-block h-2 w-2 rounded-full bg-emerald-400" />
systemd · active
</div>
</div>
</div>
)
}
7 changes: 7 additions & 0 deletions apps/docs/src/components/landing/animations/light-band.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function LightBandArrow() {
return (
<div aria-hidden className="relative mx-2 hidden h-px flex-1 self-center bg-white/10 md:block">
<span className="light-band absolute -top-px h-[3px] w-1/3 rounded-full bg-gradient-to-r from-transparent via-amber-300 to-transparent shadow-[0_0_12px_2px_rgba(255,179,0,0.4)]" />
</div>
)
}
103 changes: 103 additions & 0 deletions apps/docs/src/components/landing/animations/mini-dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
export function MiniDashboard() {
return (
<div
aria-label="Animated demo of a ServerBee server card"
className="relative w-full max-w-md rounded-2xl border border-white/10 bg-white/[0.03] p-5 shadow-2xl shadow-amber-500/5 backdrop-blur"
role="img"
>
<header className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="pulse-dot inline-block h-2.5 w-2.5 rounded-full bg-emerald-400" />
<span className="font-medium text-sm text-zinc-100">edge-tokyo-01</span>
</div>
<span className="rounded-md bg-white/5 px-2 py-0.5 font-mono text-xs text-zinc-400">linux/arm64</span>
</header>

<div className="grid grid-cols-2 gap-4">
<Ring color="#ffb300" label="CPU" value={42} />
<Ring color="#4cc9f0" label="MEM" value={61} />
</div>

<div className="mt-5">
<div className="mb-2 flex items-center justify-between text-xs text-zinc-400">
<span>Network</span>
<span className="font-mono text-zinc-300">↑ 2.1 MB/s · ↓ 318 KB/s</span>
</div>
<Sparkline />
</div>

<footer className="mt-4 grid grid-cols-3 gap-2 text-center text-xs">
<Stat label="Load" value="0.42" />
<Stat label="Disk" value="58%" />
<Stat label="Uptime" value="14d" />
</footer>
</div>
)
}

function Ring({ label, value, color }: { label: string; value: number; color: string }) {
const dash = 220
return (
<div className="flex items-center gap-3 rounded-xl bg-white/[0.03] p-3">
<svg aria-hidden="true" focusable="false" height="60" viewBox="0 0 80 80" width="60">
<circle cx="40" cy="40" fill="none" r="34" stroke="rgba(255,255,255,0.08)" strokeWidth="8" />
<circle
className="ring-anim"
cx="40"
cy="40"
fill="none"
r="34"
stroke={color}
strokeDasharray={dash}
strokeLinecap="round"
strokeWidth="8"
transform="rotate(-90 40 40)"
/>
</svg>
<div>
<div className="font-mono text-xs text-zinc-400">{label}</div>
<div className="font-semibold text-xl text-zinc-100">{value}%</div>
</div>
</div>
)
}
Comment on lines +38 to +63
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ring progress visualization doesn't use the value prop.

The Ring component accepts a value prop (e.g., 42, 61) but doesn't calculate strokeDashoffset to reflect that value. Currently, strokeDasharray is fixed at 220 and no strokeDashoffset is set, meaning both rings will display identically regardless of their different values.

For a proper progress ring, calculate and apply the offset:

const circumference = 2 * Math.PI * 34  // ≈ 213.6
const offset = circumference * (1 - value / 100)

Then add strokeDashoffset={offset} to the animated circle, or apply it via the .ring-anim CSS class using a CSS custom property.

🔧 Proposed fix
 function Ring({ label, value, color }: { label: string; value: number; color: string }) {
-  const dash = 220
+  const radius = 34
+  const circumference = 2 * Math.PI * radius
+  const offset = circumference * (1 - value / 100)
   return (
     <div className="flex items-center gap-3 rounded-xl bg-white/[0.03] p-3">
       <svg aria-hidden="true" focusable="false" height="60" viewBox="0 0 80 80" width="60">
         <circle cx="40" cy="40" fill="none" r="34" stroke="rgba(255,255,255,0.08)" strokeWidth="8" />
         <circle
           className="ring-anim"
           cx="40"
           cy="40"
           fill="none"
-          r="34"
+          r={radius}
           stroke={color}
-          strokeDasharray={dash}
+          strokeDasharray={circumference}
+          strokeDashoffset={offset}
           strokeLinecap="round"
           strokeWidth="8"
           transform="rotate(-90 40 40)"
         />
       </svg>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/src/components/landing/animations/mini-dashboard.tsx` around lines
38 - 63, The Ring component currently ignores the value prop when rendering
progress; calculate the circle circumference (2 * Math.PI * 34) and compute an
offset = circumference * (1 - value/100) and apply it to the animated circle
(the element with class "ring-anim") via strokeDashoffset (or by setting a CSS
custom property consumed by .ring-anim) instead of leaving a fixed visual for
all values; update the local dash/circumference calculation and set
strokeDasharray/strokeDashoffset on the circle in Ring to reflect the value
prop.


function Sparkline() {
return (
<div className="relative h-14 w-full overflow-hidden rounded-md bg-white/[0.03]">
<svg
aria-hidden="true"
className="spark-scroll absolute inset-y-0 left-0 h-full w-[200%]"
focusable="false"
preserveAspectRatio="none"
viewBox="0 0 400 56"
>
<defs>
<linearGradient id="spark-fill" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="#ffb300" stopOpacity="0.45" />
<stop offset="100%" stopColor="#ffb300" stopOpacity="0" />
</linearGradient>
</defs>
<path
d="M0 38 L20 30 L40 34 L60 22 L80 28 L100 18 L120 26 L140 14 L160 24 L180 16 L200 30 L220 22 L240 32 L260 18 L280 28 L300 20 L320 34 L340 24 L360 30 L380 22 L400 28 L400 56 L0 56 Z"
fill="url(#spark-fill)"
/>
<path
d="M0 38 L20 30 L40 34 L60 22 L80 28 L100 18 L120 26 L140 14 L160 24 L180 16 L200 30 L220 22 L240 32 L260 18 L280 28 L300 20 L320 34 L340 24 L360 30 L380 22 L400 28"
fill="none"
stroke="#ffb300"
strokeWidth="1.5"
/>
</svg>
</div>
)
}

function Stat({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-lg bg-white/[0.03] py-2">
<div className="text-[10px] text-zinc-500 uppercase tracking-wider">{label}</div>
<div className="font-mono text-sm text-zinc-200">{value}</div>
</div>
)
}
29 changes: 29 additions & 0 deletions apps/docs/src/components/landing/animations/monitor-dots.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export function MonitorDotsAnim() {
const probes = [
{ name: 'SSL', meta: 'expires in 73d' },
{ name: 'DNS', meta: 'A · CNAME' },
{ name: 'HTTP', meta: '200 · keyword OK' },
{ name: 'TCP', meta: ':443 · 18 ms' },
{ name: 'WHOIS', meta: 'renews 2027-04' }
]
return (
<div
aria-label="Animated demo of service monitors"
className="grid h-full grid-cols-1 gap-1.5 font-mono text-xs sm:grid-cols-2"
role="img"
>
{probes.map((p, i) => (
<div className="flex items-center justify-between rounded-md bg-white/[0.03] px-3 py-1.5" key={p.name}>
<div className="flex items-center gap-2">
<span
className="pulse-dot inline-block h-2 w-2 rounded-full bg-emerald-400"
style={{ animationDelay: `${i * 0.35}s` }}
/>
<span className="text-zinc-200">{p.name}</span>
</div>
<span className="text-[10px] text-zinc-500">{p.meta}</span>
</div>
))}
</div>
)
}
Loading
Loading