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>
|
||||
);
|
||||
}
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export interface NavItem {
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
href?: string;
|
||||
active?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/** Dashboard chrome: fixed sidebar + sticky topbar + scrolling content. */
|
||||
export function AppShell({
|
||||
brand,
|
||||
nav,
|
||||
topbar,
|
||||
children,
|
||||
footer,
|
||||
}: {
|
||||
brand: React.ReactNode;
|
||||
nav: NavItem[];
|
||||
topbar?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-bg text-text">
|
||||
<aside className="hidden w-60 shrink-0 flex-col border-r border-border bg-surface md:flex">
|
||||
<div className="flex h-14 items-center px-5 font-display text-lg font-bold">{brand}</div>
|
||||
<nav className="flex-1 space-y-0.5 px-3 py-3">
|
||||
{nav.map((item) => {
|
||||
const Comp = item.href ? "a" : "button";
|
||||
return (
|
||||
<Comp
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
onClick={item.onClick}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded px-3 py-2 text-sm font-medium transition-colors duration-fast [&_svg]:size-4",
|
||||
item.active ? "bg-brand/15 text-brand" : "text-muted hover:bg-surface-2/60 hover:text-text"
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</Comp>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
{footer && <div className="border-t border-border p-3 text-sm text-muted">{footer}</div>}
|
||||
</aside>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<header className="sticky top-0 z-30 flex h-14 items-center justify-between gap-4 border-b border-border bg-bg/80 px-5 backdrop-blur">
|
||||
{topbar}
|
||||
</header>
|
||||
<main className="flex-1 p-6">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
import * as React from "react";
|
||||
import { Command } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export interface CommandAction {
|
||||
id: string;
|
||||
label: string;
|
||||
group?: string;
|
||||
icon?: React.ReactNode;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
/** ⌘K palette. Opens on Cmd/Ctrl+K by default; big perceived-quality boost on any tool. */
|
||||
export function CommandPalette({
|
||||
actions,
|
||||
placeholder = "Type a command or search…",
|
||||
}: {
|
||||
actions: CommandAction[];
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
setOpen((o) => !o);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => document.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
|
||||
const groups = React.useMemo(() => {
|
||||
const map = new Map<string, CommandAction[]>();
|
||||
for (const a of actions) {
|
||||
const g = a.group ?? "Actions";
|
||||
map.set(g, [...(map.get(g) ?? []), a]);
|
||||
}
|
||||
return [...map.entries()];
|
||||
}, [actions]);
|
||||
|
||||
return (
|
||||
<Command.Dialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
label="Command palette"
|
||||
className={cn(
|
||||
"fixed left-1/2 top-1/4 z-50 w-full max-w-xl -translate-x-1/2 overflow-hidden",
|
||||
"rounded-lg border border-border bg-surface shadow-lg data-[state=open]:animate-scale-in"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 border-b border-border px-4">
|
||||
<Search className="size-4 text-muted" />
|
||||
<Command.Input placeholder={placeholder} className="h-12 flex-1 bg-transparent text-sm text-text outline-none placeholder:text-muted" />
|
||||
</div>
|
||||
<Command.List className="max-h-80 overflow-auto p-2">
|
||||
<Command.Empty className="py-6 text-center text-sm text-muted">No results.</Command.Empty>
|
||||
{groups.map(([group, items]) => (
|
||||
<Command.Group key={group} heading={group} className="px-1 py-1 text-xs font-semibold uppercase tracking-wide text-muted [&_[cmdk-group-items]]:mt-1">
|
||||
{items.map((a) => (
|
||||
<Command.Item
|
||||
key={a.id}
|
||||
onSelect={() => { a.onSelect(); setOpen(false); }}
|
||||
className="flex cursor-pointer items-center gap-2 rounded px-2 py-2 text-sm text-text aria-selected:bg-surface-2 [&_svg]:size-4"
|
||||
>
|
||||
{a.icon}
|
||||
{a.label}
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
))}
|
||||
</Command.List>
|
||||
</Command.Dialog>
|
||||
);
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
import * as React from "react";
|
||||
import { Stagger, StaggerItem } from "../motion";
|
||||
import { SpotlightCard } from "../animated/spotlight-card";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export interface Feature {
|
||||
icon?: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function FeatureGrid({
|
||||
heading,
|
||||
features,
|
||||
columns = 3,
|
||||
className,
|
||||
}: {
|
||||
heading?: React.ReactNode;
|
||||
features: Feature[];
|
||||
columns?: 2 | 3 | 4;
|
||||
className?: string;
|
||||
}) {
|
||||
const cols = { 2: "sm:grid-cols-2", 3: "sm:grid-cols-2 lg:grid-cols-3", 4: "sm:grid-cols-2 lg:grid-cols-4" }[columns];
|
||||
return (
|
||||
<section className={cn("mx-auto max-w-6xl px-6 py-20", className)}>
|
||||
{heading && <h2 className="mb-10 text-center font-display text-3xl font-bold tracking-tight">{heading}</h2>}
|
||||
<Stagger className={cn("grid grid-cols-1 gap-4", cols)}>
|
||||
{features.map((f) => (
|
||||
<StaggerItem key={f.title}>
|
||||
<SpotlightCard className="h-full">
|
||||
{f.icon && (
|
||||
<div className="mb-4 inline-flex size-10 items-center justify-center rounded bg-brand/15 text-brand [&_svg]:size-5">
|
||||
{f.icon}
|
||||
</div>
|
||||
)}
|
||||
<h3 className="font-display text-lg font-semibold">{f.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted">{f.description}</p>
|
||||
</SpotlightCard>
|
||||
</StaggerItem>
|
||||
))}
|
||||
</Stagger>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
import * as React from "react";
|
||||
import { FadeIn } from "../motion";
|
||||
import { Button } from "../components/button";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export function Hero({
|
||||
eyebrow,
|
||||
title,
|
||||
subtitle,
|
||||
primaryCta,
|
||||
secondaryCta,
|
||||
className,
|
||||
}: {
|
||||
eyebrow?: string;
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
primaryCta?: { label: string; href?: string; onClick?: () => void };
|
||||
secondaryCta?: { label: string; href?: string; onClick?: () => void };
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<section className={cn("relative overflow-hidden bg-bg px-6 py-24 text-center", className)}>
|
||||
{/* Subtle brand aura — depth, not a purple gradient. */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 top-0 h-[420px] opacity-60"
|
||||
style={{ background: "radial-gradient(600px 300px at 50% 0%, hsl(var(--brand) / 0.14), transparent 70%)" }}
|
||||
/>
|
||||
<div className="relative mx-auto max-w-3xl">
|
||||
{eyebrow && (
|
||||
<FadeIn>
|
||||
<span className="mb-4 inline-block rounded-sm bg-brand/15 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-brand">
|
||||
{eyebrow}
|
||||
</span>
|
||||
</FadeIn>
|
||||
)}
|
||||
<FadeIn delay={0.05}>
|
||||
<h1 className="font-display text-4xl font-bold leading-[1.1] tracking-tight sm:text-6xl">{title}</h1>
|
||||
</FadeIn>
|
||||
{subtitle && (
|
||||
<FadeIn delay={0.1}>
|
||||
<p className="mx-auto mt-6 max-w-xl text-lg text-muted">{subtitle}</p>
|
||||
</FadeIn>
|
||||
)}
|
||||
{(primaryCta || secondaryCta) && (
|
||||
<FadeIn delay={0.15}>
|
||||
<div className="mt-10 flex items-center justify-center gap-3">
|
||||
{primaryCta && (
|
||||
<Button size="lg" asChild={!!primaryCta.href} onClick={primaryCta.onClick}>
|
||||
{primaryCta.href ? <a href={primaryCta.href}>{primaryCta.label}</a> : primaryCta.label}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryCta && (
|
||||
<Button size="lg" variant="outline" asChild={!!secondaryCta.href} onClick={secondaryCta.onClick}>
|
||||
{secondaryCta.href ? <a href={secondaryCta.href}>{secondaryCta.label}</a> : secondaryCta.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</FadeIn>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
import * as React from "react";
|
||||
import { Check } from "lucide-react";
|
||||
import { Stagger, StaggerItem } from "../motion";
|
||||
import { Button } from "../components/button";
|
||||
import { Badge } from "../components/badge";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export interface PricingTier {
|
||||
name: string;
|
||||
price: string;
|
||||
period?: string;
|
||||
description?: string;
|
||||
features: string[];
|
||||
cta: { label: string; href?: string; onClick?: () => void };
|
||||
featured?: boolean;
|
||||
}
|
||||
|
||||
export function Pricing({
|
||||
heading,
|
||||
tiers,
|
||||
className,
|
||||
}: {
|
||||
heading?: React.ReactNode;
|
||||
tiers: PricingTier[];
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<section className={cn("mx-auto max-w-5xl px-6 py-20", className)}>
|
||||
{heading && <h2 className="mb-10 text-center font-display text-3xl font-bold tracking-tight">{heading}</h2>}
|
||||
<Stagger className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{tiers.map((t) => (
|
||||
<StaggerItem key={t.name}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full flex-col rounded-lg border bg-surface p-6 transition-shadow duration-slow",
|
||||
t.featured ? "border-brand/50 shadow-glow" : "border-border shadow-sm hover:shadow-md"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-display text-lg font-semibold">{t.name}</h3>
|
||||
{t.featured && <Badge variant="brand">Popular</Badge>}
|
||||
</div>
|
||||
{t.description && <p className="mt-1 text-sm text-muted">{t.description}</p>}
|
||||
<div className="mt-5 flex items-baseline gap-1">
|
||||
<span className="font-display text-4xl font-bold">{t.price}</span>
|
||||
{t.period && <span className="text-sm text-muted">/{t.period}</span>}
|
||||
</div>
|
||||
<ul className="mt-6 flex-1 space-y-2.5">
|
||||
{t.features.map((f) => (
|
||||
<li key={f} className="flex items-start gap-2 text-sm text-text">
|
||||
<Check className="mt-0.5 size-4 shrink-0 text-brand" />
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button
|
||||
className="mt-6 w-full"
|
||||
variant={t.featured ? "primary" : "outline"}
|
||||
asChild={!!t.cta.href}
|
||||
onClick={t.cta.onClick}
|
||||
>
|
||||
{t.cta.href ? <a href={t.cta.href}>{t.cta.label}</a> : t.cta.label}
|
||||
</Button>
|
||||
</div>
|
||||
</StaggerItem>
|
||||
))}
|
||||
</Stagger>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center gap-1 rounded-sm px-2 py-0.5 text-xs font-medium",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
brand: "bg-brand/15 text-brand",
|
||||
neutral: "bg-surface-2 text-muted",
|
||||
success: "bg-success/15 text-success",
|
||||
warning: "bg-warning/15 text-warning",
|
||||
danger: "bg-danger/15 text-danger",
|
||||
outline: "border border-border text-text",
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: "neutral" },
|
||||
}
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLSpanElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
export const Badge = ({ className, variant, ...props }: BadgeProps) => (
|
||||
<span className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
export { badgeVariants };
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded font-display font-semibold " +
|
||||
"transition-[background-color,box-shadow,transform,color] duration-base ease-out " +
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-bg " +
|
||||
"active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: "bg-brand text-bg hover:bg-brand-bright shadow-sm hover:shadow-glow",
|
||||
secondary: "bg-surface-2 text-text hover:bg-surface-2/70 shadow-sm",
|
||||
outline: "border border-border bg-transparent text-text hover:bg-surface-2/60",
|
||||
ghost: "bg-transparent text-text hover:bg-surface-2/60",
|
||||
danger: "bg-danger text-white hover:bg-danger/90 shadow-sm",
|
||||
link: "text-brand underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
sm: "h-8 px-3 text-sm",
|
||||
md: "h-10 px-4 text-sm",
|
||||
lg: "h-12 px-6 text-base",
|
||||
icon: "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: "primary", size: "md" },
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return <Comp ref={ref} className={cn(buttonVariants({ variant, size }), className)} {...props} />;
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
export { buttonVariants };
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("rounded-lg border border-border bg-surface shadow-sm", className)} {...props} />
|
||||
)
|
||||
);
|
||||
Card.displayName = "Card";
|
||||
|
||||
export const CardHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col gap-1 p-5", className)} {...props} />
|
||||
);
|
||||
export const CardTitle = ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h3 className={cn("font-display text-lg font-semibold leading-tight", className)} {...props} />
|
||||
);
|
||||
export const CardDescription = ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
|
||||
<p className={cn("text-sm text-muted", className)} {...props} />
|
||||
);
|
||||
export const CardContent = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("p-5 pt-0", className)} {...props} />
|
||||
);
|
||||
export const CardFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex items-center gap-2 p-5 pt-0", className)} {...props} />
|
||||
);
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export const Dialog = DialogPrimitive.Root;
|
||||
export const DialogTrigger = DialogPrimitive.Trigger;
|
||||
export const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm",
|
||||
"data-[state=open]:animate-fade-in data-[state=closed]:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = "DialogOverlay";
|
||||
|
||||
export const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2",
|
||||
"rounded-lg border border-border bg-surface p-6 shadow-lg",
|
||||
"data-[state=open]:animate-scale-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm text-muted transition-colors hover:text-text focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60">
|
||||
<X className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
));
|
||||
DialogContent.displayName = "DialogContent";
|
||||
|
||||
export const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col gap-1.5 pb-4", className)} {...props} />
|
||||
);
|
||||
export const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title ref={ref} className={cn("font-display text-lg font-semibold", className)} {...props} />
|
||||
));
|
||||
DialogTitle.displayName = "DialogTitle";
|
||||
export const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted", className)} {...props} />
|
||||
));
|
||||
DialogDescription.displayName = "DialogDescription";
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
import * as React from "react";
|
||||
import * as Menu from "@radix-ui/react-dropdown-menu";
|
||||
import { Check } from "lucide-react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export const DropdownMenu = Menu.Root;
|
||||
export const DropdownMenuTrigger = Menu.Trigger;
|
||||
export const DropdownMenuGroup = Menu.Group;
|
||||
export const DropdownMenuSeparator = ({ className, ...props }: React.ComponentPropsWithoutRef<typeof Menu.Separator>) => (
|
||||
<Menu.Separator className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
|
||||
);
|
||||
|
||||
export const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof Menu.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof Menu.Content>
|
||||
>(({ className, sideOffset = 6, ...props }, ref) => (
|
||||
<Menu.Portal>
|
||||
<Menu.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[10rem] overflow-hidden rounded-md border border-border bg-surface p-1 shadow-lg",
|
||||
"data-[state=open]:animate-scale-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</Menu.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = "DropdownMenuContent";
|
||||
|
||||
export const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof Menu.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof Menu.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Menu.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center gap-2 rounded px-2 py-1.5 text-sm text-text outline-none",
|
||||
"transition-colors focus:bg-surface-2 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = "DropdownMenuItem";
|
||||
|
||||
export const DropdownMenuLabel = ({ className, ...props }: React.ComponentPropsWithoutRef<typeof Menu.Label>) => (
|
||||
<Menu.Label className={cn("px-2 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted", className)} {...props} />
|
||||
);
|
||||
export const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof Menu.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof Menu.CheckboxItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<Menu.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn("relative flex cursor-pointer select-none items-center rounded py-1.5 pl-7 pr-2 text-sm outline-none focus:bg-surface-2", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<Menu.ItemIndicator><Check className="size-3.5" /></Menu.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</Menu.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
||||
({ className, type, ...props }, ref) => (
|
||||
<input
|
||||
type={type}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded border border-border bg-bg/40 px-3 py-2 text-sm text-text",
|
||||
"placeholder:text-muted transition-shadow duration-base ease-out",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:border-brand/60",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export const Table = ({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) => (
|
||||
<div className="relative w-full overflow-auto rounded-lg border border-border">
|
||||
<table className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
||||
</div>
|
||||
);
|
||||
export const TableHeader = ({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) => (
|
||||
<thead className={cn("[&_tr]:border-b [&_tr]:border-border", className)} {...props} />
|
||||
);
|
||||
export const TableBody = ({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) => (
|
||||
<tbody className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
||||
);
|
||||
export const TableRow = ({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
|
||||
<tr className={cn("border-b border-border transition-colors hover:bg-surface-2/50 data-[state=selected]:bg-surface-2", className)} {...props} />
|
||||
);
|
||||
export const TableHead = ({ className, ...props }: React.ThHTMLAttributes<HTMLTableCellElement>) => (
|
||||
<th className={cn("sticky top-0 z-10 bg-surface px-4 py-3 text-left font-display text-xs font-semibold uppercase tracking-wide text-muted", className)} {...props} />
|
||||
);
|
||||
export const TableCell = ({ className, ...props }: React.TdHTMLAttributes<HTMLTableCellElement>) => (
|
||||
<td className={cn("px-4 py-3 align-middle", className)} {...props} />
|
||||
);
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
import { Toaster as Sonner, toast } from "sonner";
|
||||
|
||||
/** Pre-themed sonner Toaster. Drop <Toaster /> once near your app root. */
|
||||
export const Toaster = (props: React.ComponentProps<typeof Sonner>) => (
|
||||
<Sonner
|
||||
theme="dark"
|
||||
position="bottom-right"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast: "!bg-surface !border-border !text-text !rounded-lg !shadow-lg",
|
||||
description: "!text-muted",
|
||||
actionButton: "!bg-brand !text-bg",
|
||||
cancelButton: "!bg-surface-2 !text-muted",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export { toast };
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
// Utilities
|
||||
export { cn } from "./lib/cn";
|
||||
export { tokens, type Tokens } from "./lib/tokens";
|
||||
export { setTheme, toggleTheme } from "./lib/theme";
|
||||
|
||||
// Primitives
|
||||
export { Button, buttonVariants, type ButtonProps } from "./components/button";
|
||||
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "./components/card";
|
||||
export { Input } from "./components/input";
|
||||
export { Badge, badgeVariants, type BadgeProps } from "./components/badge";
|
||||
export {
|
||||
Dialog, DialogTrigger, DialogClose, DialogContent,
|
||||
DialogHeader, DialogTitle, DialogDescription,
|
||||
} from "./components/dialog";
|
||||
export {
|
||||
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
|
||||
DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, DropdownMenuCheckboxItem,
|
||||
} from "./components/dropdown-menu";
|
||||
export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "./components/table";
|
||||
export { Toaster, toast } from "./components/toast";
|
||||
|
||||
// Motion
|
||||
export { FadeIn, Stagger, StaggerItem, staggerContainer, staggerItem, motion } from "./motion";
|
||||
|
||||
// Animated
|
||||
export { AnimatedNumber } from "./animated/animated-number";
|
||||
export { SpotlightCard } from "./animated/spotlight-card";
|
||||
|
||||
// Blocks
|
||||
export { AppShell, type NavItem } from "./blocks/app-shell";
|
||||
export { CommandPalette, type CommandAction } from "./blocks/command-palette";
|
||||
export { Hero } from "./blocks/hero";
|
||||
export { FeatureGrid, type Feature } from "./blocks/feature-grid";
|
||||
export { Pricing, type PricingTier } from "./blocks/pricing";
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
/** Merge conditional class names, de-duplicating conflicting Tailwind utilities. */
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
/** Toggle the .light class on <html>. Dark is the default (:root). */
|
||||
export function setTheme(mode: "dark" | "light") {
|
||||
const el = document.documentElement;
|
||||
el.classList.toggle("light", mode === "light");
|
||||
}
|
||||
export function toggleTheme() {
|
||||
const isLight = document.documentElement.classList.toggle("light");
|
||||
return isLight ? "light" : "dark";
|
||||
}
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Design tokens — the single source of truth for the brand.
|
||||
* These mirror the CSS variables in styles/globals.css and feed the Tailwind preset.
|
||||
* Change the brand once, here, and every app that consumes the preset updates.
|
||||
*/
|
||||
export const tokens = {
|
||||
color: {
|
||||
// Neutrals are tinted (warm), never pure black/gray — an Impeccable anti-slop rule.
|
||||
bg: "#232022",
|
||||
surface: "#2b282a",
|
||||
surface2: "#333031",
|
||||
border: "#3a3638",
|
||||
text: "#F5F1EC",
|
||||
muted: "#A99F97",
|
||||
// Brand — warm gold on warm dark.
|
||||
brand: "#DCBB4F",
|
||||
brandBright: "#F5CD15",
|
||||
brandDark: "#998643",
|
||||
// Semantic
|
||||
success: "#3F9E6A",
|
||||
warning: "#DCBB4F",
|
||||
danger: "#C0562F",
|
||||
info: "#5B8DB8",
|
||||
},
|
||||
radius: { sm: "6px", md: "10px", lg: "14px", xl: "20px" },
|
||||
font: {
|
||||
display: '"Montserrat", system-ui, sans-serif',
|
||||
sans: '"Open Sans", system-ui, sans-serif',
|
||||
mono: '"JetBrains Mono", ui-monospace, monospace',
|
||||
},
|
||||
// Layered, tinted shadows — depth instead of a flat plane.
|
||||
shadow: {
|
||||
sm: "0 1px 2px 0 rgb(20 16 18 / 0.20)",
|
||||
md: "0 4px 12px -2px rgb(20 16 18 / 0.28), 0 2px 4px -2px rgb(20 16 18 / 0.20)",
|
||||
lg: "0 12px 32px -8px rgb(20 16 18 / 0.40), 0 4px 8px -4px rgb(20 16 18 / 0.24)",
|
||||
glow: "0 0 0 1px rgb(220 187 79 / 0.20), 0 8px 28px -6px rgb(220 187 79 / 0.28)",
|
||||
},
|
||||
// Motion — smooth, never bouncy/elastic (Impeccable anti-slop rule).
|
||||
ease: {
|
||||
out: "cubic-bezier(0.22, 1, 0.36, 1)",
|
||||
inOut: "cubic-bezier(0.65, 0, 0.35, 1)",
|
||||
},
|
||||
duration: { fast: "120ms", base: "180ms", slow: "320ms" },
|
||||
} as const;
|
||||
|
||||
export type Tokens = typeof tokens;
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
import { motion, useReducedMotion, type Variants } from "motion/react";
|
||||
|
||||
const easeOut = [0.22, 1, 0.36, 1] as const;
|
||||
|
||||
/** Fade + rise on mount. Respects prefers-reduced-motion. */
|
||||
export function FadeIn({
|
||||
children,
|
||||
delay = 0,
|
||||
y = 8,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
y?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const reduce = useReducedMotion();
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
initial={reduce ? false : { opacity: 0, y }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10% 0px" }}
|
||||
transition={{ duration: 0.4, ease: easeOut, delay }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Stagger container — pair with <StaggerItem>. */
|
||||
export const staggerContainer: Variants = {
|
||||
hidden: {},
|
||||
show: { transition: { staggerChildren: 0.06 } },
|
||||
};
|
||||
export const staggerItem: Variants = {
|
||||
hidden: { opacity: 0, y: 10 },
|
||||
show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: easeOut } },
|
||||
};
|
||||
|
||||
export function Stagger({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, margin: "-10% 0px" }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
export function StaggerItem({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return <motion.div className={className} variants={staggerItem}>{children}</motion.div>;
|
||||
}
|
||||
|
||||
export { motion };
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
/* Import once in your app entry: import "@jason/ui-kit/styles.css"; */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/*
|
||||
* Brand tokens as HSL channels so Tailwind's <alpha-value> works.
|
||||
* :root = dark (default brand). .light overrides for light surfaces.
|
||||
*/
|
||||
@layer base {
|
||||
:root {
|
||||
--bg: 330 4% 13%;
|
||||
--surface: 330 4% 16%;
|
||||
--surface-2: 330 3% 20%;
|
||||
--border: 330 3% 22%;
|
||||
--text: 33 33% 94%;
|
||||
--muted: 28 9% 63%;
|
||||
--brand: 46 68% 59%;
|
||||
--brand-bright: 51 91% 52%;
|
||||
--brand-dark: 46 40% 43%;
|
||||
--success: 146 43% 43%;
|
||||
--warning: 46 68% 59%;
|
||||
--danger: 17 61% 47%;
|
||||
--info: 205 37% 54%;
|
||||
}
|
||||
|
||||
.light {
|
||||
--bg: 36 33% 97%;
|
||||
--surface: 0 0% 100%;
|
||||
--surface-2: 36 20% 95%;
|
||||
--border: 33 12% 86%;
|
||||
--text: 330 6% 15%;
|
||||
--muted: 330 4% 40%;
|
||||
}
|
||||
|
||||
* { border-color: hsl(var(--border)); }
|
||||
body {
|
||||
background-color: hsl(var(--bg));
|
||||
color: hsl(var(--text));
|
||||
font-family: "Open Sans", system-ui, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
h1, h2, h3, h4, h5 { font-family: "Montserrat", system-ui, sans-serif; }
|
||||
|
||||
/* Respect reduced-motion everywhere. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user