chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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> </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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user