feat: initial @jason/ui-kit — tokens, preset, components, motion, blocks, Impeccable
ci / build-and-design (push) Failing after 15s
ci / build-and-design (push) Failing after 15s
This commit is contained in:
Executable
+29
@@ -0,0 +1,29 @@
|
||||
import * as React from "react";
|
||||
import { useInView, useMotionValue, useSpring, useReducedMotion } from "motion/react";
|
||||
|
||||
/** Count-up number for dashboards/stat cards. Springs to `value` when scrolled into view. */
|
||||
export function AnimatedNumber({
|
||||
value,
|
||||
format = (n) => Math.round(n).toLocaleString(),
|
||||
className,
|
||||
}: {
|
||||
value: number;
|
||||
format?: (n: number) => string;
|
||||
className?: string;
|
||||
}) {
|
||||
const ref = React.useRef<HTMLSpanElement>(null);
|
||||
const inView = useInView(ref, { once: true, margin: "-20% 0px" });
|
||||
const reduce = useReducedMotion();
|
||||
const mv = useMotionValue(0);
|
||||
const spring = useSpring(mv, { stiffness: 90, damping: 20 });
|
||||
const [display, setDisplay] = React.useState(format(0));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (reduce) { setDisplay(format(value)); return; }
|
||||
if (inView) mv.set(value);
|
||||
}, [inView, value, reduce, mv, format]);
|
||||
|
||||
React.useEffect(() => spring.on("change", (v) => setDisplay(format(v))), [spring, format]);
|
||||
|
||||
return <span ref={ref} className={className}>{display}</span>;
|
||||
}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
/** Card with a soft brand glow that tracks the cursor. Depth without gimmick. */
|
||||
export function SpotlightCard({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const [pos, setPos] = React.useState({ x: 50, y: 50, active: false });
|
||||
|
||||
const onMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const r = ref.current?.getBoundingClientRect();
|
||||
if (!r) return;
|
||||
setPos({ x: ((e.clientX - r.left) / r.width) * 100, y: ((e.clientY - r.top) / r.height) * 100, active: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
onMouseMove={onMove}
|
||||
onMouseLeave={() => setPos((p) => ({ ...p, active: false }))}
|
||||
className={cn("group relative overflow-hidden rounded-lg border border-border bg-surface p-6 shadow-sm transition-shadow duration-slow hover:shadow-md", className)}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-slow group-hover:opacity-100"
|
||||
style={{
|
||||
background: `radial-gradient(400px circle at ${pos.x}% ${pos.y}%, hsl(var(--brand) / 0.10), transparent 60%)`,
|
||||
}}
|
||||
/>
|
||||
<div className="relative">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user