chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:47 +08:00
commit 7653f56fed
1422 changed files with 359026 additions and 0 deletions
@@ -0,0 +1,32 @@
.root {
@apply inline-flex text-center items-center leading-7;
}
.root span {
@apply bg-zinc-400 rounded-full h-2 w-2;
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
margin: 0 2px;
}
.root span:nth-of-type(2) {
animation-delay: 0.2s;
}
.root span:nth-of-type(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
@@ -0,0 +1,14 @@
import s from "./LoadingDots.module.css";
import cn from "classnames";
const LoadingDots = ({ className }: { className?: string }) => {
return (
<span className={cn(s.root, className)}>
<span />
<span />
<span />
</span>
);
};
export default LoadingDots;
@@ -0,0 +1 @@
export { default } from "./LoadingDots";
@@ -0,0 +1,116 @@
import Button from "./shadcn/button";
export function ButtonDemo() {
return (
<div className="p-8 space-y-8 bg-white">
<div>
<h2 className="text-title-h5 mb-4">Button Component - Two Variants</h2>
<p className="text-body-medium text-black-alpha-64 mb-6">
Unified button system with only two variants: Primary (orange/heat) and Secondary (grey)
</p>
</div>
{/* Primary Buttons */}
<div className="space-y-4">
<h3 className="text-label-large text-accent-black">Primary Variant (Orange)</h3>
<div className="flex items-center gap-12">
<Button variant="primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2"></rect>
<path d="m8 12 4-4 4 4"></path>
<path d="M12 16V8"></path>
</svg>
<span>Upgrade</span>
</Button>
<Button variant="primary">
<span>Get Started</span>
</Button>
<Button variant="primary" disabled>
<span>Disabled</span>
</Button>
<Button variant="primary" isLoading>
<span>Loading</span>
</Button>
</div>
</div>
{/* Secondary Buttons */}
<div className="space-y-4">
<h3 className="text-label-large text-accent-black">Secondary Variant (Grey)</h3>
<div className="flex items-center gap-12">
<Button variant="secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-16 h-16">
<path d="M10.268 21a2 2 0 0 0 3.464 0"></path>
<path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"></path>
</svg>
<span className="relative">
Notifications
<span className="absolute -right-4 -top-10 flex h-20 w-20 items-center justify-center rounded-full bg-heat-100 text-white text-10 font-semibold z-10">1</span>
</span>
</Button>
<Button variant="secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-16 h-16">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<path d="M12 17h.01"></path>
</svg>
<span>Help</span>
</Button>
<Button variant="secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"></path>
<path d="M14 2v4a2 2 0 0 0 2 2h4"></path>
<path d="M10 9H8"></path>
<path d="M16 13H8"></path>
<path d="M16 17H8"></path>
</svg>
<span>Docs</span>
</Button>
<Button variant="secondary" disabled>
<span>Disabled</span>
</Button>
<Button variant="secondary" isLoading>
<span>Loading</span>
</Button>
</div>
</div>
{/* Mixed Usage Example */}
<div className="space-y-4">
<h3 className="text-label-large text-accent-black">Mixed Usage Example</h3>
<div className="flex items-center gap-12">
<Button variant="secondary">Cancel</Button>
<Button variant="primary">Save Changes</Button>
</div>
</div>
{/* Size Variations */}
<div className="space-y-4">
<h3 className="text-label-large text-accent-black">Size Variations</h3>
<div className="flex items-center gap-12">
<div className="space-y-2">
<p className="text-label-small text-black-alpha-64">Default Size</p>
<div className="flex gap-4">
<Button variant="secondary" size="default">Secondary</Button>
<Button variant="primary" size="default">Primary</Button>
</div>
</div>
<div className="space-y-2">
<p className="text-label-small text-black-alpha-64">Large Size (Default)</p>
<div className="flex gap-4">
<Button variant="secondary" size="large">Secondary</Button>
<Button variant="primary" size="large">Primary</Button>
</div>
</div>
</div>
</div>
</div>
);
}
+133
View File
@@ -0,0 +1,133 @@
import { ButtonHTMLAttributes, forwardRef, ReactNode } from "react";
import { cn } from "@/utils/cn";
/**
* Unified Button Component
*
* Only two variants:
* - primary: Orange/heat color for primary actions
* - secondary: Grey for secondary actions
*
* @example
* // Primary button (orange)
* <Button variant="primary">Save Changes</Button>
*
* // Secondary button (grey) - default
* <Button>Cancel</Button>
*
* // With icon
* <Button variant="primary">
* <UploadIcon className="w-16 h-16" />
* <span>Upload</span>
* </Button>
*/
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
size?: "default" | "large";
isLoading?: boolean;
loadingLabel?: string;
children: ReactNode;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = "secondary",
size = "large",
isLoading = false,
loadingLabel = "Loading…",
disabled,
className,
children,
...props
},
ref
) => {
const isNonInteractive = Boolean(disabled || isLoading);
return (
<button
ref={ref}
type={props.type ?? "button"}
disabled={isNonInteractive}
aria-disabled={isNonInteractive || undefined}
aria-busy={isLoading || undefined}
aria-live={isLoading ? "polite" : undefined}
data-state={
isLoading ? "loading" : isNonInteractive ? "disabled" : "idle"
}
className={cn(
// Base styles
"flex items-center justify-center relative",
"text-label-medium font-medium",
"transition-all duration-200",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
// Size variants
size === "default" && "rounded-8 px-10 py-6 gap-4 text-13",
size === "large" && "rounded-10 px-12 py-8 gap-6 text-14",
// Primary variant (orange/heat)
variant === "primary" && [
"bg-heat-100 text-white",
"shadow-[0px_-6px_12px_0px_rgba(255,0,0,0.2)_inset,0px_2px_4px_0px_rgba(255,77,0,0.12)]",
!isNonInteractive && [
"hover:bg-heat-90",
"hover:shadow-[0px_-6px_12px_0px_rgba(255,0,0,0.2)_inset,0px_4px_8px_0px_rgba(255,77,0,0.16)]",
"active:scale-[0.995]",
],
"disabled:opacity-80 disabled:cursor-not-allowed",
"focus-visible:ring-white",
],
// Secondary variant (grey)
variant === "secondary" && [
"bg-black-alpha-4 text-accent-black",
!isNonInteractive && [
"hover:bg-black-alpha-6",
"active:scale-[0.99] active:bg-black-alpha-7",
],
"disabled:bg-black-alpha-3 disabled:text-black-alpha-48 disabled:cursor-not-allowed",
"focus-visible:ring-black",
],
// Custom className
className
)}
{...props}
>
{/* Primary variant gradient overlay */}
{variant === "primary" && (
<div className="absolute inset-0 rounded-inherit bg-gradient-to-b from-white/6 to-transparent pointer-events-none" />
)}
{/* Loading spinner */}
{isLoading && (
<div
className={cn(
"w-16 h-16 border-2 rounded-full animate-spin",
variant === "primary"
? "border-white/30 border-t-white"
: "border-black/30 border-t-black"
)}
aria-hidden
/>
)}
{/* Screen reader loading label */}
{isLoading && <span className="sr-only">{loadingLabel}</span>}
{/* Button content */}
<div className="relative flex items-center gap-inherit">
{children}
</div>
</button>
);
}
);
Button.displayName = "Button";
export default Button;
export { Button, type ButtonProps };
+114
View File
@@ -0,0 +1,114 @@
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
import javascript from "react-syntax-highlighter/dist/esm/languages/prism/javascript";
import "@/styles/components/code.css";
export default function Code({
code,
language,
showLineNumbers = true,
}: {
code: string;
language: string;
showLineNumbers?: boolean;
}) {
return (
<SyntaxHighlighter
customStyle={{}}
language={language}
showLineNumbers={showLineNumbers}
useInlineStyles={false}
>
{code}
</SyntaxHighlighter>
);
}
json.displayName = "json";
json.aliases = ["webmanifest"];
function json(Prism: any) {
// https://www.json.org/json-en.html
Prism.languages.json = {
property: {
pattern: /(?<=")(?:\\.|[^"\\\r\n])*(?="\s*:)/,
greedy: true,
},
punctuation: {
pattern: /[{}[\],":]/,
greedy: true,
},
string: {
pattern: /(?<=")(?:\\.|[^"\\\r\n])*(?=")/,
greedy: true,
},
comment: {
pattern: /\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,
greedy: true,
},
number: /-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,
operator: /:/,
boolean: /\b(?:false|true)\b/,
null: {
pattern: /\bnull\b/,
alias: "keyword",
},
};
Prism.languages.webmanifest = Prism.languages.json;
}
python.displayName = "python";
python.aliases = ["py", "gyp", "ipython"];
function python(Prism: any) {
Prism.languages.python = {
comment: {
pattern: /(^|[^\\])#.*/,
lookbehind: true,
greedy: true,
},
string: {
pattern: /("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,
greedy: true,
},
punctuation: /[{}[\];(),.:"'`]/,
function: {
pattern: /((?:^|\s)(?:Firecrawl|scrape_url)\b)/gm,
greedy: true,
},
keyword:
/\b(?:and|as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,
boolean: /\b(?:True|False|None)\b/,
number:
/\b(?:\d+(?:\.\d+)?(?:e[+-]?\d+)?|0b[01]+|0o[0-7]+|0x[a-fA-F0-9]+)\b/i,
operator:
/[+%=]=?|!=|<=|>=|<<|>>|\*\*?|\/\/?|\/|&|\||\^|~|\b(?:is|is not|in|not in)\b/,
};
}
curl.displayName = "curl";
curl.aliases = ["bash", "shell"];
function curl(Prism: any) {
Prism.languages.curl = {
comment: {
pattern: /#.*/,
greedy: true,
},
string: {
pattern: /(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,
greedy: true,
},
punctuation: /[{}[\];(),.:"'`$]/,
function: {
pattern: /\b(?:curl|POST|GET|PUT|DELETE|PATCH)\b/i,
},
keyword: /\b(?:Authorization|Bearer|Content-Type|application\/json)\b/i,
operator: /[=<>!&|]/,
number: /\b\d+\b/,
};
}
SyntaxHighlighter.registerLanguage("json", json);
SyntaxHighlighter.registerLanguage("python", python);
SyntaxHighlighter.registerLanguage("javascript", javascript);
SyntaxHighlighter.registerLanguage("curl", curl);
@@ -0,0 +1,56 @@
/* eslint-disable @next/next/no-img-element */
import { ComponentProps } from "react";
import compressorConfig from "@/public/compressor.json";
interface Props extends ComponentProps<"img"> {
src: string;
alt: string;
raw?: boolean;
}
const BASE_SRC = "/assets/";
const RAW_SRC = "/assets-original/";
export default function Image({ src, raw, ...attrs }: Props) {
if (raw) {
return (
<img
{...attrs}
alt={attrs.alt}
decoding="async"
loading="lazy"
src={RAW_SRC + src + ".png"}
/>
);
}
return (
<picture>
{compressorConfig.configs
.sort((a, b) => {
if (a.extension === "avif" && b.extension !== "avif") return -1;
if (b.extension === "avif" && a.extension !== "avif") return 1;
return a.scale - b.scale;
})
.map((c) => {
return (
<source
key={`${c.extension}_q${c.quality}@${c.scale}x`}
srcSet={`${BASE_SRC}${src}_q${c.quality}@${c.scale}x.${c.extension}`}
type={`image/${c.extension}`}
/>
);
})}
<img
{...attrs}
alt={attrs.alt}
decoding="async"
loading="lazy"
src={`${BASE_SRC}${src}.png`}
/>
</picture>
);
}
+28
View File
@@ -0,0 +1,28 @@
// UI Components
export * from "./menu";
export * from "./spinner";
export * from "./shadcn/button";
export * from "./shadcn/dropdown-menu";
export * from "./shadcn/tooltip";
export * from "./shadcn/dialog";
export * from "./shadcn/sheet";
export * from "./shadcn/badge";
export * from "./shadcn/checkbox";
export * from "./shadcn/input";
export * from "./shadcn/label";
export * from "./shadcn/select";
export * from "./shadcn/switch";
export * from "./shadcn/textarea";
export * from "./shadcn/toast";
export * from "./shadcn/card";
export * from "./shadcn/popover";
export * from "./shadcn/separator";
export * from "./shadcn/alert-dialog";
export * from "./shadcn/collapsible";
export * from "./shadcn/context-menu";
export * from "./shadcn/navigation-menu";
export * from "./shadcn/progress";
export * from "./shadcn/scroll-area";
export * from "./shadcn/slider";
export * from "./shadcn/toggle";
// export * from "./tremor/line-chart"; // Not used
@@ -0,0 +1,67 @@
"use client";
import { useEffect, useRef } from "react";
import data from "@/components/shared/effects/flame/hero-flame-data.json";
import { cn } from "@/utils/cn";
import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
function LoadingDashboardFlame({ flameClassName }: { flameClassName: string }) {
const ref = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let index = 0;
const interval = setIntervalOnVisible({
element: wrapperRef.current,
callback: () => {
index++;
if (index >= data.length) index = 0;
if (ref.current) {
ref.current.innerHTML = data[index];
}
},
interval: 85,
});
return () => interval?.();
}, []);
return (
<div
className="pointer-events-none select-none flex items-center justify-center"
ref={wrapperRef}
>
<div className="overflow-clip relative h-182 flex items-center justify-center">
<div
className={cn(
"text-black-alpha-32 font-ascii fc-decoration",
flameClassName,
)}
dangerouslySetInnerHTML={{ __html: data[0] }}
ref={ref}
style={{
whiteSpace: "pre",
fontSize: "9px",
lineHeight: "11px",
}}
/>
</div>
</div>
);
}
export default function LoadingDashboard({
flameClassName,
}: {
flameClassName: string;
}) {
return (
<div className="relative flex flex-col items-center justify-center min-h-screen w-full">
<LoadingDashboardFlame flameClassName={flameClassName} />
<p className="text-body-medium text-black-alpha-20 mt-24">Loading...</p>
</div>
);
}
@@ -0,0 +1,40 @@
import { CSSProperties, FC, ReactNode } from "react";
import { cn } from "@/utils/cn";
interface AnimatedShinyTextProps {
children: ReactNode;
className?: string;
shimmerWidth?: number;
}
const AnimatedShinyTextLw: FC<AnimatedShinyTextProps> = ({
children,
className,
shimmerWidth = 100,
}) => {
return (
<p
style={
{
"--shimmer-width": `${shimmerWidth}px`,
} as CSSProperties
}
className={cn(
"text-zinc-500",
// Shimmer effect
"animate-shimmer bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shimmer-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
// Shimmer gradient
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
className,
)}
>
{children}
</p>
);
};
export default AnimatedShinyTextLw;
@@ -0,0 +1,40 @@
import { CSSProperties, FC, ReactNode } from "react";
import { cn } from "@/utils/cn";
interface AnimatedShinyTextProps {
children: ReactNode;
className?: string;
shimmerWidth?: number;
}
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
children,
className,
shimmerWidth = 100,
}) => {
return (
<p
style={
{
"--shimmer-width": `${shimmerWidth}px`,
} as CSSProperties
}
className={cn(
"mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70",
// Shimmer effect
"animate-shimmer bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shimmer-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
// Shimmer gradient
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
className,
)}
>
{children}
</p>
);
};
export default AnimatedShinyText;
@@ -0,0 +1,124 @@
"use client";
import React, { PropsWithChildren, useRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { cn } from "@/utils/cn";
export interface DockProps extends VariantProps<typeof dockVariants> {
className?: string;
magnification?: number;
distance?: number;
direction?: "top" | "middle" | "bottom";
children: React.ReactNode;
}
const DEFAULT_MAGNIFICATION = 60;
const DEFAULT_DISTANCE = 125;
const dockVariants = cva(
"mx-auto w-max mt-8 h-[58px] p-2 flex gap-2 rounded-2xl supports-backdrop-blur:bg-white/10 supports-backdrop-blur:dark:bg-black/10 backdrop-blur-md dark:border-zinc-800",
);
const Dock = React.forwardRef<HTMLDivElement, DockProps>(
(
{
className,
children,
magnification = DEFAULT_MAGNIFICATION,
distance = DEFAULT_DISTANCE,
direction = "bottom",
...props
},
ref,
) => {
const mouseX = useMotionValue(Infinity);
const renderChildren = () => {
return React.Children.map(children, (child: any) => {
return React.cloneElement(child, {
mouseX: mouseX,
magnification: magnification,
distance: distance,
});
});
};
return (
<motion.div
ref={ref}
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
{...props}
className={cn(dockVariants({ className }), {
"items-start": direction === "top",
"items-center": direction === "middle",
"items-end": direction === "bottom",
})}
>
{renderChildren()}
</motion.div>
);
},
);
Dock.displayName = "Dock";
export interface DockIconProps {
size?: number;
magnification?: number;
distance?: number;
mouseX?: any;
className?: string;
children?: React.ReactNode;
props?: PropsWithChildren;
}
const DockIcon = ({
size,
magnification = DEFAULT_MAGNIFICATION,
distance = DEFAULT_DISTANCE,
mouseX,
className,
children,
...props
}: DockIconProps) => {
const ref = useRef<HTMLDivElement>(null);
const distanceCalc = useTransform(mouseX, (val: number) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});
let widthSync = useTransform(
distanceCalc,
[-distance, 0, distance],
[40, magnification, 40],
);
let width = useSpring(widthSync, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
return (
<motion.div
ref={ref}
style={{ width }}
className={cn(
"flex aspect-square cursor-pointer items-center justify-center rounded-full",
className,
)}
{...props}
>
{children}
</motion.div>
);
};
DockIcon.displayName = "DockIcon";
export { Dock, DockIcon, dockVariants };
@@ -0,0 +1,56 @@
import { useId } from "react";
import { cn } from "@/utils/cn";
interface DotPatternProps {
width?: any;
height?: any;
x?: any;
y?: any;
cx?: any;
cy?: any;
cr?: any;
className?: string;
[key: string]: any;
}
export function DotPattern({
width = 16,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
className,
...props
}: DotPatternProps) {
const id = useId();
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80",
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
patternContentUnits="userSpaceOnUse"
x={x}
y={y}
>
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
</svg>
);
}
export default DotPattern;
@@ -0,0 +1,44 @@
"use client";
import { AnimatePresence, motion, Variants } from "framer-motion";
import { cn } from "@/utils/cn";
interface GradualSpacingProps {
text: string;
duration?: number;
delayMultiple?: number;
framerProps?: Variants;
className?: string;
}
export default function GradualSpacing({
text,
duration = 0.5,
delayMultiple = 0.04,
framerProps = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 },
},
className,
}: GradualSpacingProps) {
return (
<div className="flex justify-center">
<AnimatePresence>
{text.split("").map((char, i) => (
<motion.h1
key={i}
initial="hidden"
animate="visible"
exit="hidden"
variants={framerProps}
transition={{ duration, delay: i * delayMultiple }}
className={cn("drop-shadow-sm", className)}
>
{char === " " ? <span>&nbsp;</span> : char}
</motion.h1>
))}
</AnimatePresence>
</div>
);
}
@@ -0,0 +1,47 @@
import React, { CSSProperties } from "react";
interface RippleProps {
mainCircleSize?: number;
mainCircleOpacity?: number;
numCircles?: number;
}
const Ripple = React.memo(function Ripple({
mainCircleSize = 210,
mainCircleOpacity = 0.24,
numCircles = 8,
}: RippleProps) {
return (
<div className="absolute inset-0 flex items-center justify-center bg-white/5 [mask-image:linear-gradient(to_bottom,white,transparent)]">
{Array.from({ length: numCircles }, (_, i) => {
const size = mainCircleSize + i * 70;
const opacity = mainCircleOpacity - i * 0.03;
const animationDelay = `${i * 0.06}s`;
const borderStyle = i === numCircles - 1 ? "dashed" : "solid";
const borderOpacity = 5 + i * 5;
return (
<div
key={i}
className={`absolute animate-ripple rounded-full bg-foreground/25 shadow-xl border top-1/2 left-1/2 translate-x-1/2 translate-y-1/2 [--i:${i}]`}
style={
{
width: `${size}px`,
height: `${size}px`,
opacity: opacity,
animationDelay: animationDelay,
borderStyle: borderStyle,
borderWidth: "1px",
borderColor: `rgba(var(--foreground-rgb), ${borderOpacity / 100})`,
} as CSSProperties
}
/>
);
})}
</div>
);
});
Ripple.displayName = "Ripple";
export default Ripple;
@@ -0,0 +1,37 @@
"use client";
import React from "react";
import { X } from "lucide-react";
import { cn } from "@/utils/cn";
interface MenuHeaderProps {
title: string;
onClose?: () => void;
className?: string;
}
export function MenuHeader({ title, onClose, className }: MenuHeaderProps) {
return (
<div
className={cn(
"p-12 border-b border-border-faint flex items-center justify-between",
className,
)}
>
<h2 className="text-label-medium text-black-alpha-56">{title}</h2>
{onClose && (
<button
onClick={onClose}
className={cn(
"w-24 h-24 flex items-center justify-center rounded-6 transition-all",
"text-black-alpha-40 hover:text-black-alpha-56",
"hover:bg-black-alpha-4",
"active:scale-[0.98]",
)}
>
<X className="w-12 h-12" />
</button>
)}
</div>
);
}
+565
View File
@@ -0,0 +1,565 @@
//@ts-nocheck
"use client";
import * as React from "react";
import { createPortal } from "react-dom";
import { motion, AnimatePresence } from "framer-motion";
import type { HTMLMotionProps } from "framer-motion";
import { cn } from "@/utils/cn";
import { useDropdownHover } from "@/components/hooks/use-dropdown-hover";
interface MenuContextValue {
open: boolean;
setOpen: (open: boolean) => void;
triggerRef: React.RefObject<HTMLElement>;
}
const MenuContext = React.createContext<MenuContextValue | undefined>(
undefined,
);
function useMenu() {
const context = React.useContext(MenuContext);
if (!context) {
throw new Error("useMenu must be used within a Menu");
}
return context;
}
/* -------------------------------------------------------------------------------------------------
* Menu
* -----------------------------------------------------------------------------------------------*/
interface MenuProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
children: React.ReactNode;
}
export function Menu({
open: controlledOpen,
onOpenChange,
children,
}: MenuProps) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
const triggerRef = React.useRef<HTMLElement>(null);
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen;
const setOpen = React.useCallback(
(value: boolean) => {
if (controlledOpen === undefined) {
setUncontrolledOpen(value);
}
onOpenChange?.(value);
},
[controlledOpen, onOpenChange],
);
return (
<MenuContext.Provider value={{ open, setOpen, triggerRef }}>
{children}
</MenuContext.Provider>
);
}
/* -------------------------------------------------------------------------------------------------
* MenuTrigger
* -----------------------------------------------------------------------------------------------*/
interface MenuTriggerProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
}
export const MenuTrigger = React.forwardRef<
HTMLButtonElement,
MenuTriggerProps
>(({ onClick, asChild, children, ...props }, forwardedRef) => {
const { open, setOpen, triggerRef } = useMenu();
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(e);
if (!e.defaultPrevented) {
setOpen(!open);
}
};
const setRefs = React.useCallback(
(node: HTMLElement | null) => {
if (forwardedRef) {
if (typeof forwardedRef === "function") {
forwardedRef(node as HTMLButtonElement);
} else {
forwardedRef.current = node as HTMLButtonElement;
}
}
(triggerRef as React.MutableRefObject<HTMLElement | null>).current = node;
},
[forwardedRef, triggerRef],
);
if (asChild && React.isValidElement(children)) {
const childOnClick = (children as any).props?.onClick as
| ((e: React.MouseEvent<any>) => void)
| undefined;
return React.cloneElement(children as any, {
...props,
ref: setRefs,
onClick: (e: React.MouseEvent<any>) => {
// Invoke child's onClick first so it can prevent default behavior
childOnClick?.(e);
if (e.defaultPrevented) return;
// Fall back to MenuTrigger's own click handler
handleClick(e as any);
},
});
}
return (
<button ref={setRefs} onClick={handleClick} {...props}>
{children}
</button>
);
});
MenuTrigger.displayName = "MenuTrigger";
/* -------------------------------------------------------------------------------------------------
* MenuContent
* -----------------------------------------------------------------------------------------------*/
interface MenuContentProps extends Omit<HTMLMotionProps<"div">, "ref"> {
align?: "start" | "center" | "end";
side?: "top" | "bottom" | "left" | "right";
sideOffset?: number;
alignOffset?: number;
matchTriggerWidth?: boolean;
}
function MenuPortal({
children,
open,
}: {
children: React.ReactNode;
open: boolean;
}) {
const [shouldRender, setShouldRender] = React.useState(false);
React.useEffect(() => {
if (open) {
setShouldRender(true);
}
}, [open]);
if (!shouldRender) return null;
return createPortal(
<AnimatePresence
onExitComplete={() => {
if (!open) {
setShouldRender(false);
}
}}
>
{children}
</AnimatePresence>,
document.body,
);
}
export const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(
(
{
className,
align = "start",
side = "bottom",
sideOffset = 8,
alignOffset = 0,
matchTriggerWidth = false,
children,
...props
},
ref,
) => {
const { open, setOpen, triggerRef } = useMenu();
const [position, setPosition] = React.useState<{
top: number;
left: number;
} | null>(null);
const [menuWidth, setMenuWidth] = React.useState<number>(0);
const menuRef = React.useRef<HTMLDivElement | null>(null);
React.useEffect(() => {
if (open && triggerRef.current) {
const updatePosition = () => {
if (!triggerRef.current) return;
const rect = triggerRef.current.getBoundingClientRect();
const menuPosition = { top: 0, left: 0 };
// Calculate vertical position based on side
if (side === "top") {
menuPosition.top = rect.top - sideOffset;
} else if (side === "bottom") {
menuPosition.top = rect.bottom + sideOffset;
} else if (side === "left" || side === "right") {
menuPosition.top = rect.top;
}
// Calculate horizontal position based on side and alignment
if (side === "left") {
menuPosition.left = rect.left - sideOffset;
} else if (side === "right") {
menuPosition.left = rect.right + sideOffset;
} else if (side === "top" || side === "bottom") {
// For top/bottom placement, handle alignment
if (align === "start") {
menuPosition.left = rect.left + alignOffset;
} else if (align === "center") {
menuPosition.left = rect.left + rect.width / 2;
} else if (align === "end") {
menuPosition.left = rect.right + alignOffset;
}
}
setPosition(menuPosition);
if (matchTriggerWidth) {
setMenuWidth(rect.width);
}
};
// Initial position calculation
updatePosition();
// Update position on scroll/resize
window.addEventListener("scroll", updatePosition, true);
window.addEventListener("resize", updatePosition);
return () => {
window.removeEventListener("scroll", updatePosition, true);
window.removeEventListener("resize", updatePosition);
};
}
}, [
open,
side,
align,
sideOffset,
alignOffset,
matchTriggerWidth,
triggerRef,
]);
// Separate effect to handle menu width calculations after render
React.useEffect(() => {
if (open && position && menuRef.current && triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
const menuRect = menuRef.current.getBoundingClientRect();
// Adjust position for end alignment
if (align === "end" && (side === "top" || side === "bottom")) {
setPosition((prev) => ({
...prev!,
left: rect.right - menuRect.width + alignOffset,
}));
} else if (
align === "center" &&
(side === "top" || side === "bottom")
) {
setPosition((prev) => ({
...prev!,
left: rect.left + rect.width / 2 - menuRect.width / 2,
}));
}
}
}, [open, position, align, side, alignOffset]);
return (
<MenuPortal open={open}>
{open && position && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[90]"
onClick={() => setOpen(false)}
/>
{/* Menu */}
<motion.div
ref={(node: HTMLDivElement | null) => {
if (ref) {
if (typeof ref === "function") {
ref(node);
}
}
menuRef.current = node;
}}
initial={{ opacity: 0, y: -6, scale: 1, filter: "blur(1px)" }}
animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, y: 8, scale: 0.98, filter: "blur(1px)" }}
transition={{
ease: [0.1, 0.1, 0.25, 1],
duration: 0.2,
}}
className={cn(
"fixed z-[100] min-w-[8rem] overflow-hidden rounded-16 border border-border-faint bg-white",
className,
)}
style={{
top: position.top,
left: position.left,
boxShadow:
"0px 12px 24px rgba(0, 0, 0, 0.08), 0px 4px 8px rgba(0, 0, 0, 0.04)",
width: matchTriggerWidth && menuWidth ? menuWidth : undefined,
transform: (() => {
let transforms = [];
// Handle vertical transforms for top/bottom sides
if (side === "top") {
transforms.push("translateY(-100%)");
}
// Handle horizontal transforms for left/right sides
if (side === "left") {
transforms.push("translateX(-100%)");
}
// No transform needed for align="end" since we calculate position directly
if (side === "top" || side === "bottom") {
if (align === "center") {
// Center alignment already handled in position calculation
}
} else if (side === "left" || side === "right") {
if (align === "center") {
transforms.push("translateY(-50%)");
} else if (align === "end") {
transforms.push("translateY(-100%)");
}
}
return transforms.join(" ") || undefined;
})(),
}}
{...props}
>
{children}
</motion.div>
</>
)}
</MenuPortal>
);
},
);
MenuContent.displayName = "MenuContent";
/* -------------------------------------------------------------------------------------------------
* MenuItem
* -----------------------------------------------------------------------------------------------*/
interface MenuItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
inset?: boolean;
variant?: "default" | "danger" | "active" | "ghost";
}
export const MenuItem = React.forwardRef<HTMLButtonElement, MenuItemProps>(
(
{ className, inset, onClick, variant = "default", children, ...props },
ref,
) => {
const { setOpen } = useMenu();
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(e);
if (!e.defaultPrevented) {
setOpen(false);
}
};
const baseStyles =
"relative flex w-full cursor-pointer select-none items-center gap-8 rounded-12 px-8 py-6 min-h-[36px] text-sm outline-none transition-all data-[disabled]:pointer-events-none data-[disabled]:opacity-50 active:scale-[0.98]";
const variantStyles = {
default:
"text-foreground-dimmer hover:text-foreground hover:bg-transparent",
danger: "text-red-600/70 hover:text-red-600 hover:bg-red-50",
active: "text-heat-100 bg-heat-8 hover:bg-heat-12",
ghost: "hover:bg-transparent",
};
return (
<button
ref={ref}
className={cn(
baseStyles,
variantStyles[variant],
inset && "pl-14",
className,
)}
onClick={handleClick}
{...props}
>
{children}
</button>
);
},
);
MenuItem.displayName = "MenuItem";
/* -------------------------------------------------------------------------------------------------
* MenuLabel
* -----------------------------------------------------------------------------------------------*/
interface MenuLabelProps extends React.HTMLAttributes<HTMLDivElement> {
inset?: boolean;
}
export const MenuLabel = React.forwardRef<HTMLDivElement, MenuLabelProps>(
({ className, inset, ...props }, ref) => (
<div
ref={ref}
className={cn(
"px-8 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
),
);
MenuLabel.displayName = "MenuLabel";
/* -------------------------------------------------------------------------------------------------
* MenuSeparator
* -----------------------------------------------------------------------------------------------*/
export const MenuSeparator = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("mx-0 my-1 h-px bg-border-faint", className)}
{...props}
/>
));
MenuSeparator.displayName = "MenuSeparator";
/* -------------------------------------------------------------------------------------------------
* MenuGroup
* -----------------------------------------------------------------------------------------------*/
interface MenuGroupProps extends React.HTMLAttributes<HTMLDivElement> {
withHoverBackground?: boolean;
}
export const MenuGroup = React.forwardRef<HTMLDivElement, MenuGroupProps>(
({ className, withHoverBackground = true, children, ...props }, ref) => {
const { backgroundRef, onMouseEnter, onMouseLeave, onClick } =
useDropdownHover();
if (withHoverBackground) {
// Clone children and add hover handlers
const enhancedChildren = React.Children.map(children, (child) => {
if (React.isValidElement(child) && child.type === MenuItem) {
const isActive = child.props.variant === "active";
const isDanger = child.props.variant === "danger";
// Don't apply hover background to active or danger items
const shouldApplyHoverBg = !isActive && !isDanger;
return React.cloneElement(child as React.ReactElement<any>, {
onMouseEnter: shouldApplyHoverBg
? (e: React.MouseEvent<HTMLButtonElement>) => {
onMouseEnter(e);
child.props.onMouseEnter?.(e);
}
: child.props.onMouseEnter,
onMouseLeave: shouldApplyHoverBg
? (e: React.MouseEvent<HTMLButtonElement>) => {
onMouseLeave();
child.props.onMouseLeave?.(e);
}
: child.props.onMouseLeave,
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
if (shouldApplyHoverBg) onClick(e);
child.props.onClick?.(e);
},
});
}
return child;
});
return (
<div
ref={ref}
className={cn("relative space-y-2 px-8 py-2", className)}
{...props}
>
<div
className="absolute top-0 left-8 right-8 opacity-0 z-[1] pointer-events-none bg-black-alpha-4 rounded-12 h-36"
ref={backgroundRef}
/>
{enhancedChildren}
</div>
);
}
return (
<div
ref={ref}
className={cn("space-y-2 px-8 py-2", className)}
{...props}
>
{children}
</div>
);
},
);
MenuGroup.displayName = "MenuGroup";
/* -------------------------------------------------------------------------------------------------
* MenuHeader
* -----------------------------------------------------------------------------------------------*/
export { MenuHeader } from "./menu-header";
/*
* Example Usage:
*
* <Menu>
* <MenuTrigger asChild>
* <button>Open Menu</button>
* </MenuTrigger>
* <MenuContent className="w-256">
* <MenuHeader title="Title" onClose={() => setOpen(false)} />
*
* <MenuGroup>
* <MenuItem onClick={handleAction}>
* <Icon className="w-16 h-16" />
* <span>Action Item</span>
* </MenuItem>
*
* <MenuItem variant="active">
* <span>Active Item</span>
* </MenuItem>
*
* <MenuSeparator className="!my-2" />
*
* <MenuItem variant="danger" onClick={handleDelete}>
* <span>Delete</span>
* </MenuItem>
* </MenuGroup>
* </MenuContent>
* </Menu>
*
* MenuGroup automatically handles hover background animations
* MenuItem has built-in styling with variants: default, active, danger, ghost
* All padding and spacing is handled centrally
*/
@@ -0,0 +1,48 @@
import { ReactNode } from "react";
import { motion, AnimatePresence } from "motion/react";
export interface ModalProps {
children?: ReactNode;
contentClassName?: string;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}
export default function Modal({ children, isOpen, setIsOpen }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="!fixed inset-0 z-[1000] flex items-center justify-center p-4"
>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={() => setIsOpen(false)}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="max-w-lg w-full h-max overflow-y-auto max-h-[calc(100vh-64px)] bg-accent-white relative rounded-16 z-10"
style={{
boxShadow:
"0px 32px 40px 6px rgba(0, 0, 0, 0.08), 0px 12px 32px 0px rgba(0, 0, 0, 0.06), 0px 24px 32px -8px rgba(0, 0, 0, 0.04), 0px 8px 16px -2px rgba(0, 0, 0, 0.04), 0px 0px 0px 1px rgba(0, 0, 0, 0.08)",
}}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
@@ -0,0 +1,102 @@
import { useEffect, useRef, useState } from "react";
const CHARACTERS = "!@#$%^&*()_+-=[]{}|;:,.<>?";
export default function ScrambleText({
text,
delay,
duration = 1.5,
isInView,
}: {
text: string;
delay: number;
duration?: number;
isInView: boolean;
}) {
const [displayText, setDisplayText] = useState(text); // Initialize with final text to avoid hydration mismatch
const [isClient, setIsClient] = useState(false);
const intervalRef = useRef<NodeJS.Timeout | undefined>(undefined);
const startTimeRef = useRef<number | undefined>(undefined);
const [hasAnimated, setHasAnimated] = useState(false);
const containerRef = useRef<HTMLSpanElement>(null);
// Set client-side flag after mount
useEffect(() => {
setIsClient(true);
}, []);
// Set initial width
useEffect(() => {
if (containerRef.current) {
containerRef.current.style.visibility = "hidden";
containerRef.current.textContent = text;
const width = containerRef.current.offsetWidth;
containerRef.current.textContent = displayText;
containerRef.current.style.width = `${width}px`;
containerRef.current.style.display = "inline-block";
containerRef.current.style.visibility = "visible";
}
}, [text]);
// Start animation only on client after initial render
useEffect(() => {
if (!isClient || !isInView || hasAnimated) return;
// Set initial scrambled text
setDisplayText(
Array(text.length)
.fill(0)
.map(() => CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)])
.join(""),
);
const timeoutId = setTimeout(() => {
startTimeRef.current = Date.now();
const updateText = () => {
const elapsed = Date.now() - (startTimeRef.current || 0);
const progress = Math.min(elapsed / (duration * 1000), 1);
if (progress < 1) {
const scrambledLength = Math.floor((1 - progress) * text.length);
const finalLength = text.length - scrambledLength;
setDisplayText(
text.slice(0, finalLength) +
Array(scrambledLength)
.fill(0)
.map(
() =>
CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)],
)
.join(""),
);
intervalRef.current = setTimeout(updateText, 30);
} else {
setDisplayText(text);
setHasAnimated(true);
}
};
updateText();
}, delay * 1000);
return () => {
clearTimeout(timeoutId);
if (intervalRef.current) {
clearTimeout(intervalRef.current);
}
};
}, [text, delay, duration, isInView, hasAnimated, isClient]);
return (
<span
ref={containerRef}
className="inline-block whitespace-pre"
style={{ fontVariantNumeric: "tabular-nums" }}
>
{displayText}
</span>
);
}
@@ -0,0 +1,47 @@
"use client";
import React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/utils/cn";
interface TextRevealProps {
text: string;
isVisible: boolean;
className?: string;
delay?: number;
duration?: number;
}
export function TextReveal({
text,
isVisible,
className,
delay = 0,
duration = 0.3,
}: TextRevealProps) {
return (
<AnimatePresence>
{isVisible && (
<motion.span
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
}}
transition={{
duration,
delay,
ease: "easeInOut",
}}
className={cn("inline-block", className)}
>
{text}
</motion.span>
)}
</AnimatePresence>
);
}
@@ -0,0 +1,120 @@
"use client";
import { useEffect, useRef } from "react";
const padding = 8;
function Scrollbar() {
const ref = useRef<HTMLDivElement>(null);
const isDragging = useRef(false);
const startY = useRef(0);
const startScrollY = useRef(0);
useEffect(() => {
const thumbElement = ref.current!.children[0] as HTMLDivElement;
let thumbHeight = 0;
const updateBounds = () => {
const maxScrollY = document.body.scrollHeight - window.innerHeight;
thumbHeight = window.innerHeight / document.body.scrollHeight;
const scrollPosition =
padding +
(window.scrollY / maxScrollY) *
(window.innerHeight - thumbHeight * window.innerHeight - padding * 2);
Object.assign(thumbElement.style, {
height: `${thumbHeight * 100}%`,
transform: `translateY(${scrollPosition}px)`,
});
};
updateBounds();
const onMouseMove = (e: MouseEvent) => {
if (!isDragging.current) return;
const deltaY = e.clientY - startY.current;
const maxScrollY = document.body.scrollHeight - window.innerHeight;
const scrollRatio =
deltaY / (window.innerHeight - thumbHeight * window.innerHeight);
window.scrollTo({
top: startScrollY.current + scrollRatio * maxScrollY,
behavior: "instant",
});
showScrollbar();
};
let scrollbarHideTimeout: number = 0;
const showScrollbar = () => {
if (thumbHeight === 1 || window.innerWidth < 996) return;
clearTimeout(scrollbarHideTimeout);
thumbElement.classList.remove("opacity-0");
scrollbarHideTimeout = window.setTimeout(() => {
thumbElement.classList.add("opacity-0");
}, 1000);
};
const onMouseUp = () => {
isDragging.current = false;
document.body.style.userSelect = "";
thumbElement.classList.remove("!bg-black/40", "!w-8", "!right-6");
showScrollbar();
};
const onMouseDown = (e: MouseEvent) => {
isDragging.current = true;
startY.current = e.clientY;
startScrollY.current = window.scrollY;
document.body.style.userSelect = "none";
thumbElement.classList.add("!bg-black/40", "!w-8", "!right-6");
showScrollbar();
};
const onScroll = () => {
updateBounds();
showScrollbar();
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
window.addEventListener("scroll", onScroll, { passive: true });
thumbElement.addEventListener("mousedown", onMouseDown);
const container = ref.current!;
container.addEventListener("mouseenter", showScrollbar);
return () => {
window.removeEventListener("scroll", onScroll);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousedown", onMouseDown);
container.removeEventListener("mouseenter", showScrollbar);
};
}, []);
return (
<div className="fixed right-0 w-16 z-[1000] h-screen top-0" ref={ref}>
<div className="bg-black/10 opacity-0 backdrop-blur-4 hover:w-6 w-4 right-8 hover:right-7 transition-[width,right,opacity] duration-[200ms] hover:bg-black/20 absolute top-0 rounded-full" />
</div>
);
}
export default Scrollbar;
@@ -0,0 +1,58 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/utils/cn";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,167 @@
"use client";
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/utils/cn";
const AlertDialog = ({
children,
hideChatOnOpen = true,
...props
}: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Root> & {
hideChatOnOpen?: boolean;
}) => {
const handleOpenChange = (isOpen: boolean) => {
// Pass down `onOpenChange` if provided by the parent
props?.onOpenChange?.(isOpen);
};
return (
<AlertDialogPrimitive.Root {...props} onOpenChange={handleOpenChange}>
{children}
</AlertDialogPrimitive.Root>
);
};
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-[1000] bg-background-base/80 backdrop-blur-md data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<div className="fixed z-[1001] inset-0 flex items-end sm:items-center justify-center p-16 sm:p-0">
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"relative w-full max-w-[520px] border border-border-faint bg-white p-0 duration-200 sm:rounded-16 shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-bottom-10 data-[state=open]:slide-in-from-bottom-10 sm:data-[state=closed]:slide-out-to-left-1/2 sm:data-[state=closed]:slide-out-to-top-[-2%] sm:data-[state=open]:slide-in-from-left-1/2 sm:data-[state=open]:slide-in-from-top-[-2%]",
className,
)}
{...props}
>
{children}
</AlertDialogPrimitive.Content>
</div>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-6 text-center sm:text-left",
className,
)}
{...props}
/>
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-8",
className,
)}
{...props}
/>
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn(
"text-title-h5 font-semibold leading-none tracking-tight text-accent-black",
className,
)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-body-medium text-black-alpha-72", className)}
{...props}
/>
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(
// Primary button styling aligned with design system
"inline-flex items-center justify-center rounded-8 px-12 py-8 text-label-medium bg-heat-100 text-accent-white transition-colors hover:opacity-95 disabled:opacity-50 disabled:cursor-not-allowed",
className,
)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
"mt-2 sm:mt-0 inline-flex items-center justify-center rounded-8 px-12 py-8 text-label-medium text-accent-black hover:bg-black-alpha-4 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
className,
)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
@@ -0,0 +1,28 @@
/* eslint-disable react/jsx-no-comment-textnodes */
import { HTMLAttributes } from "react";
import { cn } from "@/utils/cn";
function Badge({ ...attrs }: HTMLAttributes<HTMLDivElement>) {
return (
<div
{...attrs}
className={cn(
attrs.className,
"flex gap-10 bg-background-base relative w-max pb-16 items-center text-label-x-small",
)}
>
<div className="h-1 bottom-0 absolute w-full left-0 bg-border-faint" />
<div className="text-black-alpha-16 pointer-events-none select-none">
//
</div>
<div className="relative flex gap-10 items-center">{attrs.children}</div>
<div className="text-black-alpha-16 pointer-events-none select-none -scale-x-100">
//
</div>
</div>
);
}
export default Badge;
export { Badge };
@@ -0,0 +1,80 @@
.button {
transition:
all 0.2s cubic-bezier(0.25, 0.1, 0.25, 1),
scale 0.1s cubic-bezier(0.25, 0.1, 0.25, 1),
box-shadow 0.1s cubic-bezier(0.25, 0.1, 0.25, 1);
}
.button:active {
transition:
all 0.2s cubic-bezier(0.25, 0.1, 0.25, 1),
scale 0.05s cubic-bezier(0.25, 0.1, 0.25, 1),
box-shadow 0.05s cubic-bezier(0.25, 0.1, 0.25, 1);
}
.button-primary {
background: #ff4c00;
background: color(display-p3 0.9816 0.3634 0.0984);
box-shadow:
0px -6px 12px 0px rgba(255, 0, 0, 0.2) inset,
0px 2px 4px 0px rgba(255, 77, 0, 0.12),
0px 1px 1px 0px rgba(255, 77, 0, 0.12),
0px 0.5px 0.5px 0px rgba(255, 77, 0, 0.16),
0px 0.25px 0.25px 0px rgba(255, 77, 0, 0.2);
box-shadow:
0px -6px 12px 0px color(display-p3 0.9804 0.1127 0.098 / 0.2) inset,
0px 2px 4px 0px color(display-p3 0.9804 0.3647 0.098 / 0.12),
0px 1px 1px 0px color(display-p3 0.9804 0.3647 0.098 / 0.12),
0px 0.5px 0.5px 0px color(display-p3 0.9804 0.3647 0.098 / 0.16),
0px 0.25px 0.25px 0px color(display-p3 0.9804 0.3647 0.098 / 0.2);
}
.button-primary:hover {
box-shadow:
0px -6px 12px 0px rgba(255, 0, 0, 0.2) inset,
0px 4px 8px 0px rgba(255, 77, 0, 0.16),
0px 1px 1px 0px rgba(255, 77, 0, 0.12),
0px 0.5px 0.5px 0px rgba(255, 77, 0, 0.16),
0px 0.25px 0.25px 0px rgba(255, 77, 0, 0.2);
box-shadow:
0px -6px 12px 0px color(display-p3 0.9804 0.1127 0.098 / 0.2) inset,
0px 4px 8px 0px color(display-p3 0.9804 0.3647 0.098 / 0.16),
0px 1px 1px 0px color(display-p3 0.9804 0.3647 0.098 / 0.12),
0px 0.5px 0.5px 0px color(display-p3 0.9804 0.3647 0.098 / 0.16),
0px 0.25px 0.25px 0px color(display-p3 0.9804 0.3647 0.098 / 0.2);
}
.button-primary:active {
box-shadow:
0px -6px 12px 0px rgba(255, 0, 0, 0.2) inset,
0px 2px 4px 0px rgba(255, 77, 0, 0.12),
0px 1px 1px 0px rgba(255, 77, 0, 0.12),
0px 0.5px 0.5px 0px rgba(255, 77, 0, 0.16),
0px 0.25px 0.25px 0px rgba(255, 77, 0, 0.2);
box-shadow:
0px -6px 12px 0px color(display-p3 0.9804 0.1127 0.098 / 0.2) inset,
0px 2px 4px 0px color(display-p3 0.9804 0.3647 0.098 / 0.12),
0px 1px 1px 0px color(display-p3 0.9804 0.3647 0.098 / 0.12),
0px 0.5px 0.5px 0px color(display-p3 0.9804 0.3647 0.098 / 0.16),
0px 0.25px 0.25px 0px color(display-p3 0.9804 0.3647 0.098 / 0.2);
}
.button-background {
background: linear-gradient(to bottom, white, transparent);
opacity: 0.06;
transition: opacity 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
}
.button:hover .button-background {
opacity: 0.08;
}
.button:active .button-background {
opacity: 0;
transition: opacity 0.05s cubic-bezier(0.25, 0.1, 0.25, 1);
}
@@ -0,0 +1,114 @@
import { ButtonHTMLAttributes, forwardRef } from "react";
import { cn } from "@/utils/cn";
import "./button.css";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
size?: "default" | "large";
disabled?: boolean;
loadingLabel?: string;
isLoading?: boolean;
}
const Button = forwardRef<HTMLButtonElement, Props>(
(
{
variant = "secondary",
size = "large",
disabled,
isLoading = false,
loadingLabel = "Loading…",
...attrs
},
ref,
) => {
const isNonInteractive = Boolean(disabled || isLoading);
// Focus ring adapts to light/dark variants
const focusRing =
variant === "primary"
? "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white"
: "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-black";
return (
<button
{...attrs}
ref={ref}
type={attrs.type ?? "button"}
aria-disabled={isNonInteractive || undefined}
aria-busy={isLoading || undefined}
aria-live={isLoading ? "polite" : undefined}
data-state={
isLoading ? "loading" : isNonInteractive ? "disabled" : "idle"
}
className={cn(
attrs.className,
"flex items-center justify-center button relative [&>*]:relative",
"text-label-medium lg-max:[&_svg]:size-24",
`button-${variant} group/button`,
focusRing,
// Shared non-interactive styles
"disabled:cursor-not-allowed",
isNonInteractive && "cursor-not-allowed",
// Size - default to large as shown in the example
size === "default" && "rounded-8 px-10 py-6 gap-4",
size === "large" && "rounded-10 px-12 py-8 gap-6",
// Primary variant (orange/heat)
variant === "primary" && [
"text-accent-white",
// Hover/active only when interactive
!isNonInteractive &&
"hover:bg-[color:var(--heat-90)] active:[scale:0.995]",
// Disabled: dim a bit, no hover, dim overlay bg layer if present
"disabled:opacity-80",
"disabled:[&_.button-background]:opacity-70",
],
// Secondary variant (grey)
variant === "secondary" && [
"text-accent-black",
!isNonInteractive && "active:[scale:0.99] active:bg-black-alpha-7",
"bg-black-alpha-4",
!isNonInteractive && "hover:bg-black-alpha-6",
// Disabled: lighter fill + muted text, no hover
"disabled:bg-black-alpha-3",
"disabled:text-black-alpha-48",
"disabled:hover:bg-black-alpha-3",
],
)}
disabled={isNonInteractive}
>
{variant === "primary" && (
<div className="overlay button-background !absolute" />
)}
{/* loading state (spinner) */}
{isLoading && (
<div
className={cn(
"w-16 h-16 border-2 rounded-full animate-spin",
variant === "primary"
? "border-white/30 border-t-white"
: "border-black/30 border-t-black",
)}
aria-hidden
/>
)}
{/* Screen reader-only loading label */}
{isLoading && <span className="sr-only">{loadingLabel}</span>}
{attrs.children}
</button>
);
},
);
Button.displayName = "Button";
export default Button;
@@ -0,0 +1,83 @@
import * as React from "react";
import { cn } from "@/utils/cn";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-12 border border-border-muted bg-background-base",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col gap-4 p-16", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-heading-4 font-semibold text-foreground", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-body-medium text-foreground-dimmer", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-16 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-16 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
@@ -0,0 +1,57 @@
import { AnimatePresence, motion } from "motion/react";
import { cn } from "@/utils/cn";
export default function Checkbox({
checked,
onChange,
}: {
checked: boolean;
onChange?: (checked: boolean) => void;
}) {
return (
<button
className="size-20 p-3 relative"
type="button"
onClick={() => onChange?.(!checked)}
>
<div
className={cn(
"w-full h-full rounded-3 group before:inside-border relative transition-all",
checked
? "bg-heat-100 group-hover:bg-[#FA4500] before:border-transparent"
: "bg-black-alpha-3 group-hover:bg-black-alpha-6 before:border-black-alpha-10 group-hover:before:border-black-alpha-40",
)}
style={{
boxShadow: checked
? "0px 2px 4px 0px rgba(255, 77, 0, 0.12), 0px 1px 1px 0px rgba(255, 77, 0, 0.12), 0px 0.5px 0.5px 0px rgba(255, 77, 0, 0.16), 0px 0.25px 0.25px 0px rgba(255, 77, 0, 0.20)"
: "",
}}
>
<AnimatePresence>
{checked && (
<motion.svg
animate={{ opacity: 1, scale: 1, y: 0 }}
className="absolute cs-10"
exit={{ opacity: 0, scale: 0.9, y: 4 }}
fill="none"
height="10"
initial={{ opacity: 0, scale: 0.9, y: 4 }}
viewBox="0 0 10 10"
width="10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 5.98438L4.39062 8.375L8.375 2"
stroke="white"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.25"
/>
</motion.svg>
)}
</AnimatePresence>
</div>
</button>
);
}
@@ -0,0 +1,11 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
@@ -0,0 +1,174 @@
import { animate, AnimatePresence, cubicBezier, motion } from "motion/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { cn } from "@/utils/cn";
import { lockBody } from "@/components/shared/lockBody";
import PortalToBody from "@/components/shared/utils/portal-to-body";
export default function Combobox({
placeholder,
options,
value,
onChange,
className,
}: {
placeholder?: string;
options: { label: string; value: string }[];
value: string;
onChange: (value: string) => void;
className?: string;
}) {
const selected = useMemo(() => {
return options.find((option) => option.value === value);
}, [options, value]);
const [isOpen, setIsOpen] = useState(false);
const [bounds, setBounds] = useState<DOMRect | null>(null);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
lockBody("combobox", isOpen);
}, [isOpen]);
useEffect(() => {
document.addEventListener("click", (e) => {
if (ref.current && e.composedPath().includes(ref.current)) {
return;
}
setIsOpen(false);
});
}, []);
return (
<div className={cn("w-full", className)} ref={ref}>
<button
className={cn(
"relative bg-accent-white flex w-full gap-4 rounded-8 p-6 pl-10",
"before:inside-border before:border-black-alpha-8 hover:before:border-black-alpha-12 hover:bg-black-alpha-2",
"text-body-medium",
isOpen &&
"!bg-accent-white before:!border-heat-100 before:!border-[1.25px]",
)}
type="button"
onClick={(e) => {
e.preventDefault();
setIsOpen(!isOpen);
setBounds(ref.current?.getBoundingClientRect() ?? null);
}}
>
<div className={cn("flex-1", !selected && "text-black-alpha-40")}>
{selected?.label || placeholder}
</div>
<motion.svg
animate={{ rotate: isOpen ? 180 : 0 }}
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 8.5L10 11.5L13 8.5"
stroke="#262626"
strokeLinecap="round"
strokeLinejoin="round"
strokeOpacity="0.56"
strokeWidth="1.25"
/>
</motion.svg>
</button>
<PortalToBody>
<AnimatePresence initial={false}>
{isOpen && bounds && (
<motion.div
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
className="fixed bg-accent-white rounded-12 z-[401]"
exit={{ opacity: 0, y: 0, filter: "blur(4px)" }}
initial={{ opacity: 0, y: -12, filter: "blur(4px)" }}
style={{
top: bounds.top + bounds.height + 8,
left: bounds.left,
width: bounds.width,
boxShadow:
"0px 32px 40px 6px rgba(0, 0, 0, 0.02), 0px 12px 32px 0px rgba(0, 0, 0, 0.02), 0px 24px 32px -8px rgba(0, 0, 0, 0.02), 0px 8px 16px -2px rgba(0, 0, 0, 0.02), 0px 0px 0px 1px rgba(0, 0, 0, 0.04)",
}}
transition={{ duration: 0.2 }}
>
<div className="p-4">
<Items
options={options}
onChange={(value) => {
onChange(value);
setIsOpen(false);
}}
/>
</div>
</motion.div>
)}
</AnimatePresence>
</PortalToBody>
</div>
);
}
const Items = ({
options,
onChange,
}: {
options: { label: string; value: string }[];
onChange: (value: string) => void;
}) => {
const backgroundRef = useRef<HTMLDivElement>(null);
return (
<div className="relative">
<div
className="absolute top-0 opacity-0 left-0 bg-black-alpha-4 rounded-8 w-full h-32 pointer-events-none"
ref={backgroundRef}
/>
{options.map((option) => (
<button
className="w-full group py-6 px-10 text-label-small"
key={option.value}
type="button"
onClick={() => {
onChange(option.value);
}}
onMouseEnter={(e) => {
const t = e.target as HTMLElement;
let target =
t instanceof HTMLButtonElement
? t
: (t.closest("button") as HTMLButtonElement);
target = target.closest(".group") as HTMLButtonElement;
animate(backgroundRef.current!, { scale: 0.995 }).then(() =>
animate(backgroundRef.current!, { scale: 1 }),
);
animate(
backgroundRef.current!,
{
y: target.offsetTop,
opacity: 1,
},
{
ease: cubicBezier(0.165, 0.84, 0.44, 1),
duration: 0.2,
},
);
}}
onMouseLeave={() => {
animate(backgroundRef.current!, { opacity: 0 });
}}
>
{option.label}
</button>
))}
</div>
);
};
@@ -0,0 +1,200 @@
"use client";
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/utils/cn";
const ContextMenu = ContextMenuPrimitive.Root;
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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",
className,
)}
{...props}
/>
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
/>
);
};
ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};
@@ -0,0 +1,117 @@
import * as React from "react";
import { cn } from "@/utils/cn";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-zinc-100/50 [&>tr]:last:border-b-0 dark:bg-zinc-800/50",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-zinc-100/50 data-[state=selected]:bg-zinc-100 dark:hover:bg-zinc-800/50 dark:data-[state=selected]:bg-zinc-800",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle text-zinc-500 [&:has([role=checkbox])]:pr-0 dark:text-zinc-400",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-zinc-500 dark:text-zinc-400", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
@@ -0,0 +1,128 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/utils/cn";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
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-[1000] bg-background-base/80 backdrop-blur-md data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
hideCloseButton?: boolean;
}
>(({ className, children, hideCloseButton = false, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<div className="fixed z-[1001] inset-0 flex items-end sm:items-center justify-center p-16 sm:p-0">
<DialogPrimitive.Content
ref={ref}
className={cn(
"relative w-full max-w-[520px] border border-border-faint bg-white p-0 duration-200 sm:rounded-16 shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-bottom-10 data-[state=open]:slide-in-from-bottom-10 sm:data-[state=closed]:slide-out-to-left-1/2 sm:data-[state=closed]:slide-out-to-top-[-2%] sm:data-[state=open]:slide-in-from-left-1/2 sm:data-[state=open]:slide-in-from-top-[-2%]",
className,
)}
{...props}
>
{children}
{!hideCloseButton && (
<DialogPrimitive.Close className="absolute top-20 right-20 w-32 h-32 rounded-8 flex items-center justify-center hover:bg-black-alpha-4 transition-colors">
<X className="h-16 w-16 text-black-alpha-56" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</div>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-6 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-8",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-title-h5 font-semibold leading-none tracking-tight text-accent-black",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-body-medium text-black-alpha-72", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
@@ -0,0 +1,200 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/utils/cn";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-zinc-100 data-[state=open]:bg-zinc-100 dark:focus:bg-zinc-800 dark:data-[state=open]:bg-zinc-800",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-zinc-200 bg-white p-1 text-zinc-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-12 border border-border-faint bg-white p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-8 px-2 py-1.5 text-sm outline-none transition-all hover:bg-black-alpha-3 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};
@@ -0,0 +1,179 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form";
import { cn } from "@/utils/cn";
import { Label } from "./label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && "text-red-500 dark:text-red-900", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-zinc-500 dark:text-zinc-400", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm text-red-500 dark:text-red-900", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};
@@ -0,0 +1,29 @@
import { forwardRef } from "react";
import { cn } from "@/utils/cn";
const Input = forwardRef<
HTMLInputElement,
React.ComponentPropsWithoutRef<"input">
>(({ className, ...props }, ref) => {
return (
<label
className={cn(
"py-8 px-12 rounded-8 transition-all w-full block gap-4 cursor-text",
"relative bg-accent-white",
"before:inside-border before:border-black-alpha-8 hover:before:border-black-alpha-12 hover:bg-black-alpha-2 focus-within:!bg-accent-white focus-within:before:!border-heat-100 focus-within:before:!border-[1.25px]",
"text-body-medium",
className,
)}
>
<input
ref={ref}
className="outline-none w-full resize-none bg-transparent"
{...props}
/>
</label>
);
});
Input.displayName = "Input";
export default Input;
@@ -0,0 +1,26 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/cn";
const labelVariants = cva(
"text-sm leading-tight peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
@@ -0,0 +1,135 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import { cn } from "@/utils/cn";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> & {
viewportProps?: React.ComponentPropsWithoutRef<
typeof NavigationMenuPrimitive.Viewport
>;
}
>(({ className, children, viewportProps, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
<NavigationMenuViewport {...viewportProps} />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva("");
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(
navigationMenuTriggerStyle(),
"group items-center",
className,
)}
{...props}
>
{children}
{""}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute right-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border border-zinc-200 bg-white text-zinc-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)] dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-zinc-200 shadow-md dark:bg-zinc-800" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};
@@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/utils/cn";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border border-zinc-200 bg-white p-4 text-zinc-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };
@@ -0,0 +1,28 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/utils/cn";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-zinc-900 transition-all dark:bg-zinc-50"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };
@@ -0,0 +1,48 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/utils/cn";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-zinc-200 dark:bg-zinc-800" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };
@@ -0,0 +1,159 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/utils/cn";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 dark:text-white",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
@@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/utils/cn";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-zinc-200 dark:bg-zinc-800",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
@@ -0,0 +1,143 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/utils/cn";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-white p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 dark:bg-zinc-950",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-zinc-100 dark:ring-offset-zinc-950 dark:focus:ring-zinc-300 dark:data-[state=open]:bg-zinc-800">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className,
)}
{...props}
/>
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold text-zinc-950 dark:text-zinc-50",
className,
)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-zinc-500 dark:text-zinc-400", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
@@ -0,0 +1,28 @@
"use client";
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/utils/cn";
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className,
)}
{...props}
>
<SliderPrimitive.Track className="relative h-8 w-full grow overflow-hidden rounded-full bg-black-alpha-8">
<SliderPrimitive.Range className="absolute h-full bg-heat-100" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-20 w-20 rounded-full border-2 border-heat-100 bg-accent-white ring-offset-accent-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-heat-100 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = "Slider";
export { Slider };
@@ -0,0 +1,58 @@
"use client";
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/cn";
const switchVariants = cva(
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-muted-foreground data-[state=unchecked]:bg-input",
{
variants: {
size: {
default: "h-5 w-9",
sm: "h-4 w-7",
},
},
defaultVariants: {
size: "default",
},
},
);
const thumbVariants = cva(
"pointer-events-none block rounded-full bg-background shadow-lg ring-0 transition-transform",
{
variants: {
size: {
default:
"h-4 w-4 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
sm: "h-3 w-3 data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0",
},
},
defaultVariants: {
size: "default",
},
},
);
interface SwitchProps
extends React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>,
VariantProps<typeof switchVariants> {}
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
SwitchProps
>(({ className, size, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(switchVariants({ size, className }))}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb className={cn(thumbVariants({ size }))} />
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
@@ -0,0 +1,52 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/utils/cn";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex items-center justify-center gap-4 border-b border-border-faint px-16 text-body-small text-black-alpha-48",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"relative inline-flex items-center justify-center whitespace-nowrap border-b-2 border-transparent px-12 py-6 text-body-small text-black-alpha-48 transition-colors data-[state=active]:border-heat-100 data-[state=active]:text-heat-100",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn("mt-8", className)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -0,0 +1,21 @@
import { cn } from "@/utils/cn";
export default function Textarea(
textareaProps: React.ComponentPropsWithoutRef<"textarea">,
) {
return (
<label
className={cn(
"py-8 px-12 rounded-8 transition-all w-full block gap-4 cursor-text",
"relative bg-accent-white",
"before:inside-border before:border-black-alpha-8 hover:before:border-black-alpha-12 hover:bg-black-alpha-2 focus-within:!bg-accent-white focus-within:before:!border-heat-100 focus-within:before:!border-[1.25px]",
"text-body-medium",
)}
>
<textarea
className="outline-none w-full resize-none bg-transparent"
{...textareaProps}
/>
</label>
);
}
@@ -0,0 +1,31 @@
"use client";
import { Toaster as Sonner } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ theme = "light", ...props }: ToasterProps) => {
const resolvedTheme = (theme ?? "light") as ToasterProps["theme"];
return (
<Sonner
theme={resolvedTheme}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-white group-[.toaster]:text-zinc-950 group-[.toaster]:border-zinc-200 group-[.toaster]:shadow-lg dark:group-[.toaster]:bg-zinc-950 dark:group-[.toaster]:text-zinc-50 dark:group-[.toaster]:border-zinc-800",
description:
"group-[.toast]:text-zinc-500 dark:group-[.toast]:text-zinc-400",
actionButton:
"group-[.toast]:bg-zinc-900 group-[.toast]:text-zinc-50 dark:group-[.toast]:bg-zinc-50 dark:group-[.toast]:text-zinc-900",
cancelButton:
"group-[.toast]:bg-zinc-100 group-[.toast]:text-zinc-500 dark:group-[.toast]:bg-zinc-800 dark:group-[.toast]:text-zinc-400",
},
}}
{...props}
/>
);
};
export { Toaster };
@@ -0,0 +1,78 @@
import { motion } from "motion/react";
import { useState } from "react";
import { cn } from "@/utils/cn";
export default function Toggle({
checked,
onChange,
disabled,
}: {
checked: boolean;
onChange?: (checked: boolean) => void;
disabled?: boolean;
}) {
const [isHovering, setIsHovering] = useState(false);
return (
<button
className={cn(
"transition-all relative rounded-full group",
checked ? "bg-heat-100" : "bg-black-alpha-10",
)}
style={{
width: "50px",
height: "20px",
boxShadow: checked
? "0px 6px 12px 0px rgba(174, 37, 0, 0.12) inset, 0px 0.75px 0.75px 0px rgba(174, 37, 0, 0.06) inset, 0px 0.25px 0.25px 0px rgba(174, 37, 0, 0.06) inset"
: "0px 6px 12px 0px rgba(0, 0, 0, 0.02) inset, 0px 0.75px 0.75px 0px rgba(0, 0, 0, 0.02) inset, 0px 0.25px 0.25px 0px rgba(0, 0, 0, 0.04) inset",
}}
type="button"
onClick={() => onChange?.(!checked)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
disabled={disabled}
>
<div
className={cn(
"overlay bg-[#FA4500] transition-opacity",
checked
? "opacity-0 group-hover:opacity-100"
: "opacity-0 group-hover:opacity-0",
)}
style={{
background: "color(display-p3 0.9059 0.3294 0.0784)",
}}
/>
<motion.div
animate={{
x: checked ? 18 : 0,
}}
className="top-[2px] left-[2px] transition-[box-shadow] absolute rounded-full bg-accent-white"
initial={{
x: checked ? 18 : 0,
}}
style={{
width: "28px",
height: "16px",
boxShadow: (() => {
if (checked) {
if (isHovering) {
return "0px 6px 12px -3px rgba(174, 36, 0, 0.30), 0px 3px 6px -1px rgba(174, 37, 0, 0.12), 0px 1px 2px 0px rgba(174, 37, 0, 0.12), 0px 0.5px 0.5px 0px rgba(174, 37, 0, 0.24)";
}
return "0px 6px 12px -3px rgba(174, 36, 0, 0.30), 0px 3px 6px -1px rgba(174, 37, 0, 0.12), 0px 1px 2px 0px rgba(174, 37, 0, 0.12), 0px 0.5px 0.5px 0px rgba(174, 37, 0, 0.24)";
}
if (isHovering) {
return "0px 6px 12px -3px rgba(0, 0, 0, 0.06), 0px 3px 6px -1px rgba(0, 0, 0, 0.06), 0px 1px 2px 0px rgba(0, 0, 0, 0.04), 0px 0.5px 0.5px 0px rgba(0, 0, 0, 0.08)";
}
return "0px 6px 12px -3px rgba(0, 0, 0, 0.06), 0px 3px 6px -1px rgba(0, 0, 0, 0.06), 0px 1px 2px 0px rgba(0, 0, 0, 0.04), 0px 0.5px 0.5px 0px rgba(0, 0, 0, 0.08)";
})(),
}}
/>
</button>
);
}
@@ -0,0 +1,30 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/utils/cn";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border border-zinc-200 bg-white px-3 py-1.5 text-sm text-zinc-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 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 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -0,0 +1,120 @@
"use client";
import { AnimatePresence, motion } from "motion/react";
import { nanoid } from "nanoid";
import { useEffect, useRef, useState } from "react";
import PortalToBody from "@/components/shared/utils/portal-to-body";
import { cn } from "@/utils/cn";
export default function Tooltip({
delay = 0.5,
offset = 8,
wrapperClassName,
className,
...props
}: {
description?: string;
children?: React.ReactNode;
offset?: number;
delay?: number;
wrapperClassName?: string;
className?: string;
}) {
const [hovering, setHovering] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const [bounds, setBounds] = useState<{
x: number;
y: number;
width: number;
height: number;
} | null>(null);
useEffect(() => {
if (ref.current) {
const element = ref.current;
const parent = element.parentElement!;
const onMouseEnter = () => {
setBounds(parent.getBoundingClientRect());
setHovering(true);
};
const onMouseLeave = () => setHovering(false);
if (!parent) return;
parent.addEventListener("mouseenter", onMouseEnter);
parent.addEventListener("mouseleave", onMouseLeave);
return () => {
parent.removeEventListener("mouseenter", onMouseEnter);
parent.removeEventListener("mouseleave", onMouseLeave);
};
}
}, []);
if (!props.description) return props.children;
return (
<div className="contents" ref={ref}>
<PortalToBody>
<AnimatePresence initial={false} mode="popLayout">
{hovering && (
<div
className={cn(
"fixed pointer-events-none flex-center z-[121]",
wrapperClassName,
)}
style={{
left: bounds?.x,
top: bounds?.y,
width: bounds?.width,
height: bounds?.height,
}}
>
<motion.div
animate={{
y: 0,
opacity: 1,
filter: "blur(0px)",
transition: {
type: "spring",
stiffness: 240,
damping: 16,
filter: { duration: 0.4 },
delay,
},
}}
className={cn(
"py-10 px-16 rounded-12 max-w-248 absolute w-max text-body-medium text-accent-white bg-black-alpha-64 backdrop-blur-[6px] z-[121]",
className,
)}
dangerouslySetInnerHTML={{ __html: props.description }}
exit={{
y: -8,
opacity: 0,
filter: "blur(4px)",
transition: { type: "spring", stiffness: 300, damping: 16 },
}}
initial={{ y: 8, opacity: 0, filter: "blur(4px)" }}
key={nanoid()}
style={{
boxShadow:
"0px 16px 24px -8px rgba(0, 0, 0, 0.06), 0px 8px 16px -4px rgba(0, 0, 0, 0.06)",
bottom: `calc(100% - ${offset}px)`,
}}
transition={{
type: "spring",
stiffness: 160,
damping: 13,
filter: { duration: 0.4 },
}}
/>
</div>
)}
</AnimatePresence>
</PortalToBody>
</div>
);
}
@@ -0,0 +1,153 @@
import { HTMLAttributes, memo, useEffect, useRef } from "react";
import { animate } from "motion";
import { AnimatePresence, motion } from "motion/react";
import Check from "@/components/shared/icons/check";
import { cn } from "@/utils/cn";
export default memo(function Spinner({
finished,
...attrs
}: HTMLAttributes<HTMLDivElement> & { finished?: boolean }) {
return (
<div
{...attrs}
className={cn("size-20 min-w-[20px] relative", attrs.className)}
>
<AnimatePresence initial={false}>
{!finished && <Canvas />}
</AnimatePresence>
{finished && (
<motion.div
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
className="overlay"
initial={{ opacity: 0, scale: 0.9, filter: "blur(4px)" }}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
delay: 0.2,
}}
>
<Check />
</motion.div>
)}
</div>
);
});
const Canvas = memo(function Canvas() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
// Initialize canvas
const dpr = window.devicePixelRatio || 1;
canvas.width = 20 * dpr;
canvas.height = 20 * dpr;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.scale(dpr, dpr);
let activeGroup = -1;
const rects = Array.from({ length: 16 }, (_, i) => ({
x: 3 + (i % 4) * 4,
y: 3 + Math.floor(i / 4) * 4,
width: 2,
height: 2,
alpha: 0,
borderRadius: 0,
}));
let dead = false;
const grid = [
[0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 1, 0.2, 0.2, 1, 1, 0.2, 0.2, 1, 0, 1, 1, 0],
[
1, 0.4, 0.4, 1, 0.4, 0.12, 0.12, 0.4, 0.4, 0.12, 0.12, 0.4, 1, 0.4, 0.4,
1,
],
[
0.4, 0.12, 0.12, 0.4, 0.12, 0.04, 0.04, 0.12, 0.12, 0.04, 0.04, 0.12,
0.4, 0.12, 0.12, 0.4,
],
[
0.12, 0.04, 0.04, 0.12, 0.04, 0, 0, 0.04, 0.04, 0, 0, 0.04, 0.12, 0.04,
0.04, 0.12,
],
[0.04, 0, 0, 0.04, 0, 0, 0, 0, 0, 0, 0, 0, 0.04, 0, 0, 0.04],
Array.from({ length: 16 }, () => 0),
];
const render = () => {
if (dead) return;
ctx.fillStyle = "#FA5D19";
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let index = 0; index < 16; index++) {
ctx.globalAlpha = rects[index].alpha;
ctx.beginPath();
ctx.roundRect(
rects[index].x,
rects[index].y,
rects[index].width,
rects[index].height,
rects[index].borderRadius,
);
ctx.fill();
}
requestAnimationFrame(render);
};
const cycle = () => {
if (activeGroup === 2 && dead) return;
activeGroup = (activeGroup + 1) % 6;
grid[activeGroup].forEach((alpha, index) => {
animate(rects[index].alpha, alpha, {
duration: 0.05,
onUpdate: (value) => {
rects[index].alpha = value;
},
});
});
setTimeout(
() => {
if (!dead) {
cycle();
}
},
activeGroup === 5 ? 200 : 100,
);
};
cycle();
render();
canvas.addEventListener("resize", render);
return () => {
dead = true;
};
}, []);
return (
<motion.canvas
animate={{ opacity: 1, scale: 1 }}
className="absolute top-0 left-0 size-20"
exit={{ opacity: 0, scale: 0.9 }}
ref={canvasRef}
/>
);
});
+117
View File
@@ -0,0 +1,117 @@
import * as React from "react";
import { cn } from "@/utils/cn";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-zinc-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-zinc-800/50",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-zinc-100/50 data-[state=selected]:bg-zinc-100 dark:hover:bg-zinc-800/50 dark:data-[state=selected]:bg-zinc-800",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-zinc-500 [&:has([role=checkbox])]:pr-0 dark:text-zinc-400",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-zinc-500 dark:text-zinc-400", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
@@ -0,0 +1,95 @@
"use client";
import { CSSProperties, useEffect, useRef, useState } from "react";
import { cn } from "@/utils/cn";
interface VideoProps {
src?: string;
className?: string;
videoClassName?: string;
autoPlay?: boolean;
muted?: boolean;
loop?: boolean;
controls?: boolean;
poster?: string;
style?: CSSProperties;
sources?: Array<{
src: string;
type: string;
}>;
}
export default function Video({
src,
className = "",
videoClassName = "",
style = {},
autoPlay = true,
muted = true,
loop = true,
controls = false,
poster,
sources = [],
}: VideoProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
},
{ threshold: 0.1 },
);
observer.observe(containerRef.current);
return () => {
if (container) {
observer.unobserve(container);
}
};
}, []);
// Control playback based on visibility
useEffect(() => {
if (!videoRef.current) return;
if (isVisible) {
videoRef.current.play().catch(() => null);
} else {
videoRef.current.pause();
}
}, [isVisible]);
return (
<div className={className} ref={containerRef} style={style}>
<video
autoPlay={autoPlay && isVisible}
className={cn("w-full h-full", videoClassName)}
controls={controls}
loop={loop}
muted={muted}
poster={poster}
preload="auto"
ref={videoRef}
playsInline
>
{sources.length > 0 ? (
sources.map((source, index) => (
<source key={index} src={source.src} type={source.type} />
))
) : (
<source src={src} type="video/mp4" />
)}
Your browser does not support the video tag.
</video>
</div>
);
}