{
+ const element = getGlowFollowElement(event.target);
+
+ if (element) {
+ setGlowFollowPosition(element, event.clientX, event.clientY);
+ }
+ },
+ { passive: true },
+ );
+
+ document.addEventListener(
+ "mousemove",
+ (event) => {
+ const element = getGlowFollowElement(event.target);
+
+ if (element) {
+ setGlowFollowPosition(element, event.clientX, event.clientY);
+ }
+ },
+ { passive: true },
+ );
+
+ document.addEventListener(
+ "pointerout",
+ (event) => {
+ const element = getGlowFollowElement(event.target);
+
+ if (!element || (event.relatedTarget instanceof Node && element.contains(event.relatedTarget))) {
+ return;
+ }
+
+ clearGlowFollowPosition(element);
+ },
+ { passive: true },
+ );
+
+ document.addEventListener(
+ "mouseout",
+ (event) => {
+ const element = getGlowFollowElement(event.target);
+
+ if (!element || (event.relatedTarget instanceof Node && element.contains(event.relatedTarget))) {
+ return;
+ }
+
+ clearGlowFollowPosition(element);
+ },
+ { passive: true },
+ );
+}
+
+installGlowFollow();
diff --git a/src/components/hover-card.tsx b/src/components/hover-card.tsx
index 776a665..c3042a3 100644
--- a/src/components/hover-card.tsx
+++ b/src/components/hover-card.tsx
@@ -30,7 +30,7 @@ function HoverCardContent({
align={align}
sideOffset={sideOffset}
className={cn(
- "z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
+ "glass-dropdown z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-xl p-2.5 text-sm text-popover-foreground outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className
)}
{...props}
diff --git a/src/components/input.tsx b/src/components/input.tsx
index a768eee..1b0cd9c 100644
--- a/src/components/input.tsx
+++ b/src/components/input.tsx
@@ -7,7 +7,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
- "flex h-8 w-full rounded-lg border border-white/[0.09] bg-white/[0.04] px-3 py-1 text-sm transition-all placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:border-primary/40 focus-visible:bg-white/[0.06] focus-visible:shadow-[0_0_0_4px_oklch(0.80_0.17_90_/_0.08)] disabled:cursor-not-allowed disabled:opacity-50",
+ "glass-surface flex h-9 w-full rounded-lg border-white/10 px-3 py-1 text-sm text-white/90 shadow-[0_0_15px_rgba(255,255,255,0.02)] transition-all placeholder:text-white/[0.35] focus-visible:border-primary/[0.45] focus-visible:bg-white/[0.055] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:shadow-[0_0_0_4px_rgba(245,181,23,0.08),0_0_22px_rgba(245,181,23,0.06)] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
diff --git a/src/components/item.tsx b/src/components/item.tsx
index 53d0aaa..8b62bbe 100644
--- a/src/components/item.tsx
+++ b/src/components/item.tsx
@@ -34,13 +34,13 @@ function ItemSeparator({
}
const itemVariants = cva(
- "group/item flex w-full flex-wrap items-center rounded-lg border text-sm transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-muted",
+ "group/item flex w-full flex-wrap items-center rounded-lg border text-sm transition-colors duration-100 outline-none focus-visible:border-primary/[0.45] focus-visible:ring-[3px] focus-visible:ring-primary/20 [a]:transition-colors [a]:hover:bg-white/[0.04]",
{
variants: {
variant: {
- default: "border-transparent",
- outline: "border-border",
- muted: "border-transparent bg-muted/50",
+ default: "border-transparent hover:bg-white/[0.035]",
+ outline: "border-white/[0.08] bg-white/[0.02]",
+ muted: "border-white/[0.06] bg-white/[0.04]",
},
size: {
default: "gap-2.5 px-3 py-2.5",
diff --git a/src/components/metric-card.tsx b/src/components/metric-card.tsx
index 79cbc03..2702de7 100644
--- a/src/components/metric-card.tsx
+++ b/src/components/metric-card.tsx
@@ -1,34 +1,54 @@
import * as React from "react";
+import { clearGlowFollowPosition, installGlowFollow, setGlowFollowPosition } from "./glow-follow";
import { cn } from "../utils";
+// Ensure global glow-follow listeners are installed when this module loads.
+installGlowFollow();
+
interface MetricCardProps {
label: string;
value: string | number;
icon?: React.ReactNode;
delta?: { value: string; direction: "up" | "down" | "neutral" };
+ frosted?: boolean;
+ focus?: boolean;
+ spotlight?: boolean;
className?: string;
}
-const DELTA_STYLES = { up: "text-emerald-400", down: "text-red-400", neutral: "text-zinc-400" };
+const DELTA_STYLES = { up: "text-emerald-400", down: "text-red-400", neutral: "text-white/[0.45]" };
const DELTA_ARROWS = { up: "↑", down: "↓", neutral: "→" };
-export function MetricCard({ label, value, icon, delta, className }: MetricCardProps) {
+export function MetricCard({ label, value, icon, delta, frosted = true, focus = false, spotlight = false, className }: MetricCardProps) {
return (
setGlowFollowPosition(event.currentTarget, event.clientX, event.clientY)}
+ onMouseLeave={(event) => clearGlowFollowPosition(event.currentTarget)}
+ onMouseMove={(event) => setGlowFollowPosition(event.currentTarget, event.clientX, event.clientY)}
+ onPointerEnter={(event) => setGlowFollowPosition(event.currentTarget, event.clientX, event.clientY)}
+ onPointerLeave={(event) => clearGlowFollowPosition(event.currentTarget)}
+ onPointerMove={(event) => setGlowFollowPosition(event.currentTarget, event.clientX, event.clientY)}
>
- {label}
- {icon && {icon}}
+ {label}
+ {icon && (
+
+ {icon}
+
+ )}
-
-
{value}
+
+
{value}
{delta && (
-
+
{DELTA_ARROWS[delta.direction]} {delta.value}
)}
diff --git a/src/components/popover.tsx b/src/components/popover.tsx
index 3228a60..42b3cac 100644
--- a/src/components/popover.tsx
+++ b/src/components/popover.tsx
@@ -28,7 +28,7 @@ function PopoverContent({
align={align}
sideOffset={sideOffset}
className={cn(
- "z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-xl border border-white/[0.1] bg-[rgba(14,14,16,0.88)] p-2.5 text-sm text-popover-foreground [backdrop-filter:blur(20px)_saturate(1.5)] [-webkit-backdrop-filter:blur(20px)_saturate(1.5)] shadow-[0_0_0_1px_rgba(255,255,255,0.04)_inset,0_8px_32px_rgba(0,0,0,0.6)] outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
+ "glass-dropdown z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-xl p-2.5 text-sm text-popover-foreground outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className
)}
{...props}
diff --git a/src/components/progress.tsx b/src/components/progress.tsx
index 59fac6c..d7666ad 100644
--- a/src/components/progress.tsx
+++ b/src/components/progress.tsx
@@ -1,27 +1,105 @@
+"use client"
+
import * as React from "react"
import { Progress as ProgressPrimitive } from "radix-ui"
import { cn } from "../utils"
+const PARTICLES = [
+ { x: 13, y: -4, delay: -40, scale: 1 },
+ { x: 24, y: 3, delay: -220, scale: 0.75 },
+ { x: 9, y: 4, delay: -400, scale: 0.9 },
+ { x: 18, y: -3, delay: -580, scale: 0.65 },
+ { x: 29, y: -5, delay: -760, scale: 0.8 },
+ { x: 34, y: 5, delay: -940, scale: 0.7 },
+ { x: 16, y: 6, delay: -1120, scale: 0.58 },
+ { x: 38, y: -2, delay: -1300, scale: 0.72 },
+ { x: 27, y: 1, delay: -1480, scale: 0.52 },
+]
+
+const PARTICLE_DECAY_MS = 1400
+
+function particleStyle({ x, y, delay, scale }: (typeof PARTICLES)[number]) {
+ return {
+ "--progress-particle-x": `${x}px`,
+ "--progress-particle-y": `${y}px`,
+ "--progress-particle-delay": `${delay}ms`,
+ "--progress-particle-scale": scale,
+ } as React.CSSProperties
+}
+
function Progress({
className,
value,
...props
}: React.ComponentProps) {
+ const progressValue = typeof value === "number" ? Math.max(0, Math.min(100, value)) : 0
+ const [particlesActive, setParticlesActive] = React.useState(false)
+ const previousProgressValue = React.useRef(progressValue)
+ const particleTimeout = React.useRef(null)
+
+ React.useEffect(() => {
+ if (previousProgressValue.current === progressValue) {
+ return
+ }
+
+ previousProgressValue.current = progressValue
+
+ if (particleTimeout.current) {
+ window.clearTimeout(particleTimeout.current)
+ }
+
+ if (progressValue <= 0) {
+ setParticlesActive(false)
+ return
+ }
+
+ setParticlesActive(true)
+ particleTimeout.current = window.setTimeout(() => {
+ setParticlesActive(false)
+ }, PARTICLE_DECAY_MS)
+ }, [progressValue])
+
+ React.useEffect(() => {
+ return () => {
+ if (particleTimeout.current) {
+ window.clearTimeout(particleTimeout.current)
+ }
+ }
+ }, [])
+
return (
-
+
+
+
+
+
+
+ {PARTICLES.map((particle, index) => (
+
+ ))}
+
+
)
}
diff --git a/src/components/radio-group.tsx b/src/components/radio-group.tsx
index 389b492..bc407b4 100644
--- a/src/components/radio-group.tsx
+++ b/src/components/radio-group.tsx
@@ -24,7 +24,7 @@ function RadioGroupItem({
) {
return (
);
diff --git a/src/components/slider.tsx b/src/components/slider.tsx
index d4beb8c..0d5602b 100644
--- a/src/components/slider.tsx
+++ b/src/components/slider.tsx
@@ -36,18 +36,18 @@ function Slider({
>
{Array.from({ length: _values.length }, (_, index) => (
))}
diff --git a/src/components/switch.tsx b/src/components/switch.tsx
index 1e5b290..a142f3c 100644
--- a/src/components/switch.tsx
+++ b/src/components/switch.tsx
@@ -15,14 +15,14 @@ function Switch({
data-slot="switch"
data-size={size}
className={cn(
- "peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
+ "peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-white/10 shadow-[0_0_12px_rgba(255,255,255,0.03)] transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-primary/[0.45] focus-visible:ring-3 focus-visible:ring-primary/20 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] data-[state=checked]:border-primary/40 data-[state=checked]:bg-primary data-[state=checked]:shadow-[0_0_16px_rgba(245,181,23,0.25)] data-[state=unchecked]:bg-white/[0.07] data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
)
diff --git a/src/components/table.tsx b/src/components/table.tsx
index ffac59a..0d5145d 100644
--- a/src/components/table.tsx
+++ b/src/components/table.tsx
@@ -31,7 +31,7 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
tr]:last:border-b-0", className)}
+ className={cn("border-t border-white/[0.06] bg-white/[0.035] font-medium [&>tr]:last:border-b-0", className)}
{...props}
/>
);
@@ -42,7 +42,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
) {
| ) {
|