initial commit

This commit is contained in:
2026-04-26 20:28:05 +02:00
parent 5542f2cd63
commit b522fd3127
22 changed files with 2537 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
"use client";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils";
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn(" last:border-b-0", className)}
{...props}
/>
);
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between \
gap-4 rounded-md py-4 text-left text-sm font-medium outline-none hover:underline \
focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
+64
View File
@@ -0,0 +1,64 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { motion } from "motion/react";
import type * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium transition-color \
disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 \
shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] \
aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-main dark:text-neutral shadow-xs hover:bg-primary/70",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-primary/25 text-secondary-foreground shadow-xs hover:bg-primary/50 ",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
const AnimatedButton = motion.create(Button);
export { Button, buttonVariants, AnimatedButton };
+16
View File
@@ -0,0 +1,16 @@
import { createContext, useContext } from "react";
const CardDepthContext = createContext(0);
export function useCardDepth() {
return useContext(CardDepthContext);
}
export function CardDepthProvider({ children }: { children: React.ReactNode }) {
const depth = useCardDepth();
return (
<CardDepthContext.Provider value={depth + 1}>
{children}
</CardDepthContext.Provider>
);
}
+86
View File
@@ -0,0 +1,86 @@
import { Slot } from "@radix-ui/react-slot";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import { type HTMLMotionProps, m } from "motion/react";
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { CardDepthProvider, useCardDepth } from "./card-context";
const cardVariants = cva(
`flex flex-col gap-2 ring-2 rounded-card
ring-accent transition-colors grow justify-start
`,
{
variants: {
padding: {
true: "p-card",
false: "",
},
interactive: {
false: "",
true: "hover:bg-accent/30 cursor-pointer",
},
},
defaultVariants: {
interactive: false,
padding: true,
},
},
);
const BASE_RADIUS = 0.625 * 3;
const PADDING = 1.25;
const MIN_RADIUS = 0;
type CommonCardProps = {
asChild?: boolean;
className?: string;
children: ReactNode;
style?: HTMLAttributes<unknown>["style"];
} & VariantProps<typeof cardVariants>;
const Card = forwardRef<HTMLDivElement, CommonCardProps>(
({ asChild, className, interactive, padding, style, ...props }, ref) => {
const depth = useCardDepth();
const radius = Math.max(BASE_RADIUS - depth * PADDING, MIN_RADIUS); // 4px floor
const Comp = asChild ? Slot : "div";
return (
<CardDepthProvider>
<Comp
ref={ref}
style={{ borderRadius: `${radius}rem`, ...style }}
className={cn(cardVariants({ interactive, className, padding }))}
{...props}
/>
</CardDepthProvider>
);
},
);
Card.displayName = "Card";
const MCard = m.create(Card);
export const AnimatedCard: React.FC<
CommonCardProps & HTMLMotionProps<"div">
> = (props) => {
return (
<MCard
variants={{ enter: { scale: 1 }, from: { scale: 0 } }}
transition={{
type: "spring",
stiffness: 200,
damping: 15,
mass: 2,
}}
{...props}
/>
);
};
export default Card;
export { cardVariants };
+240
View File
@@ -0,0 +1,240 @@
"use client";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return;
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) return;
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) return;
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel();
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel();
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
);
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
);
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};
+148
View File
@@ -0,0 +1,148 @@
"use client";
import { XIcon } from "lucide-react";
import { motion } from "motion/react";
import { Dialog as DialogPrimitive } from "radix-ui";
import type * as React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { cardVariants } from "./card";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-100 bg-black/50 backdrop-blur-lg",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}) {
return (
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(className)}
{...props}
>
{children}
</DialogPrimitive.Content>
);
}
const AnimatedDialogContent = motion.create(DialogContent);
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean;
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
AnimatedDialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};
@@ -0,0 +1,21 @@
import type { SerializedHeadingNode } from "@payloadcms/richtext-lexical";
import type { JSXConverters } from "@payloadcms/richtext-lexical/react";
export const headingConverter: JSXConverters<SerializedHeadingNode> = {
heading: ({ node, nodesToJSX }) => {
if (node.tag === "h2") {
const text = nodesToJSX({ nodes: node.children });
const id = text
.join("")
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
return <h2 id={id}>{text}</h2>;
} else {
const text = nodesToJSX({ nodes: node.children }).join("");
const Tag = node.tag;
return <Tag>{text}</Tag>;
}
},
};
@@ -0,0 +1,25 @@
import type { DefaultNodeTypes } from "@payloadcms/richtext-lexical";
import {
type JSXConvertersFunction,
LinkJSXConverter,
} from "@payloadcms/richtext-lexical/react";
import { headingConverter } from "./headings";
import { internalDocToHref } from "./internal-link";
type NodeTypes = DefaultNodeTypes;
// Insert blocks like:
// | SerializedBlockNode<TableOfContentsProps | ContentWithMediaProps>;
export const jsxConverter: JSXConvertersFunction<NodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
...LinkJSXConverter({ internalDocToHref }),
...headingConverter,
blocks: {
/* contentWithMedia: ({ node }) => <ContentWithMedia {...node.fields} />,
tableOfContents: ({ node }) => <TableOfContents {...node.fields} />,
*/
},
});
@@ -0,0 +1,21 @@
import type { SerializedLinkNode } from "@payloadcms/richtext-lexical";
export const internalDocToHref = ({
linkNode,
}: {
linkNode: SerializedLinkNode;
}) => {
// biome-ignore lint/style/noNonNullAssertion: safe
const { value, relationTo } = linkNode.fields.doc!;
const slug =
typeof value !== "string" && typeof value !== "number" && value.slug;
if (relationTo === "posts") {
return `/posts/${slug}`;
} else if (relationTo === "users") {
return `/users/${slug}`;
} else {
return `/${slug}`;
}
};
+19
View File
@@ -0,0 +1,19 @@
import type { SerializedEditorState } from "@payloadcms/richtext-lexical/lexical";
import { RichText as RichTextConverter } from "@payloadcms/richtext-lexical/react";
import { jsxConverter } from "./converters";
type Props = {
data: SerializedEditorState;
} & React.HTMLAttributes<HTMLDivElement>;
export function RichText(props: Props) {
const { className, ...rest } = props;
return (
<RichTextConverter
{...rest}
className={className}
converters={jsxConverter}
/>
);
}
@@ -0,0 +1,96 @@
"use client";
import { ease } from "@unom/style";
import { cva, type VariantProps } from "class-variance-authority";
import { motion } from "motion/react";
import type { FC, ReactElement, ReactNode } from "react";
import Text from "@/components/ui/text";
import { cn } from "@/lib/utils";
type HeadingProps = {
/*** Is this the main heading of the page? Uses h1 if true. */
main?: boolean;
/*** Subtitle for additional information */
subtitle?: ReactElement | string;
/*** IconComponent showed above title */
IconComponent?: ReactElement;
children?: ReactNode;
className?: string;
};
const headingVariants = cva("max-w-[800px]", {
variants: {
padding: {
true: "py-4",
false: "p-0",
},
color: {
main: "text-main",
secondary: "text-main/70",
neutral: "text-neutral",
},
},
defaultVariants: {
color: "main",
},
});
const Heading: FC<HeadingProps & VariantProps<typeof headingVariants>> = ({
children,
main = false,
IconComponent,
padding,
color,
subtitle,
className,
}) => {
return (
<div className="relative mb-8">
{IconComponent && (
<>
<div
className="z-20 absolute w-full h-24 top-8 -left-8
bg-linear-to-t from-neutral to-transparent"
/>
<div className="relative z-10">
<motion.div
className="text-highlight
drop-shadow-[0px_0px_10px_hsl(var(--color-highlight)/0.5)]
w-fit h-fit"
transition={ease.quint(2.5).out}
variants={{
enter: { y: 20, scale: 1, x: -30, rotate: -15 },
from: { y: 100, scale: 0, x: 0, rotate: 0 },
}}
>
{IconComponent}
</motion.div>
</div>
</>
)}
<div className="relative z-30 mb-2">
<Text
tag={main ? "h1" : "h2"}
className={cn(
headingVariants({ className, padding, color }),
"my-4",
subtitle ? "mb-2" : main ? "mb-2" : "mb-1",
)}
>
{children}
</Text>
{subtitle && (
<Text
tag="h3"
className="max-w-[600px]"
color={color === "neutral" ? "neutral" : "secondary"}
>
{subtitle}
</Text>
)}
</div>
</div>
);
};
export default Heading;
+61
View File
@@ -0,0 +1,61 @@
"use client";
import { motion, stagger, useInView } from "motion/react";
import { createContext, type FC, useRef } from "react";
import { cn } from "@/lib/utils";
import type { SectionProps } from "./section.types";
export const InViewContext = createContext(false);
const Section: FC<SectionProps> = ({
children,
name,
extraStyles,
maxWidth = true,
extraClassNames = "",
threshold = 0,
height = "fit-content",
transition = { delayChildren: stagger(0.1) },
noPadding = false,
variants = {
container: {
enter: {},
from: {},
exit: {},
},
},
}) => {
const ref = useRef<HTMLElement>(null);
const inView = useInView(ref, { once: true, amount: threshold });
return (
<section
{...(name ? { id: name } : {})}
ref={ref}
style={{
height: height,
...extraStyles,
}}
className={cn(
"relative w-full",
noPadding ? "p-0" : "p-main",
maxWidth ? "max-w-max-section m-auto" : "",
extraClassNames,
)}
>
<InViewContext.Provider value={inView}>
<motion.div
className="h-full"
transition={transition}
initial="from"
animate={inView ? "enter" : "from"}
exit="exit"
variants={variants.container}
>
{children}
</motion.div>
</InViewContext.Provider>
</section>
);
};
export default Section;
@@ -0,0 +1,21 @@
import type { Transition, Variant } from "motion/react";
import type React from "react";
export interface SectionProps {
children: React.ReactNode;
styles?: {
container?: string;
innerContainer?: string;
};
height?: string;
threshold?: number;
extraStyles?: object;
extraClassNames?: string;
name?: string;
transition?: Transition;
noPadding?: boolean;
maxWidth?: boolean;
variants?: {
container: { enter: Variant; from: Variant; exit: Variant };
};
}
+204
View File
@@ -0,0 +1,204 @@
"use client";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { Select as SelectPrimitive } from "radix-ui";
import type * as React from "react";
import { cn } from "@/lib/utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-main data-[placeholder]:text-secondary [&_svg:not([class*='text-'])]:text-secondary focus-visible:border-ring",
"focus-visible:ring-main/50 aria-invalid:ring-error/20 dark:aria-invalid:ring-error/40 aria-invalid:border-error",
"dark:bg-neutral-accent/30 dark:hover:bg-neutral-accent/50 flex w-fit items-center justify-between gap-2 rounded-full",
"border bg-transparent px-3 py-2 text-sm text-main whitespace-nowrap shadow-xs transition-[color,box-shadow,background-color] outline-none",
"focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-input-height data-[size=sm]:h-8",
"*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center",
"*:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-neutral data-[state=open]:animate-in data-[state=closed]:animate-out p-1",
"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 relative z-100 max-h-(--radix-select-content-available-height) min-w-[8rem]",
"origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-2xl border shadow-md",
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}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-main px-3 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-main/25 text-main/75 focus:text-main [&_svg:not([class*='text-'])]:text-main transition-colors",
"relative flex w-full cursor-default items-center gap-2 rounded-full py-1.5 pr-8 pl-3",
"text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex",
"*:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-main pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};
+75
View File
@@ -0,0 +1,75 @@
"use client";
import { ease } from "@unom/style";
import { cva, type VariantProps } from "class-variance-authority";
import { type HTMLMotionProps, motion } from "motion/react";
import { type FC, forwardRef, type HTMLProps } from "react";
import { z } from "zod";
import { cn } from "@/lib/utils";
const TextTagHeadingOptions = ["h1", "h2", "h3", "h4", "h5"] as const;
const TextTagContentOptions = ["span", "p"] as const;
export const TextTag = z.enum([
...TextTagHeadingOptions,
...TextTagContentOptions,
]);
const textVariants = cva(null, {
variants: {
color: {
default: "",
main: "",
secondary: "text-secondary",
neutral: "text-neutral",
},
},
});
type Tags = z.output<typeof TextTag>;
type TextProps = {
tag: Tags;
} & HTMLProps<HTMLHeadingElement> &
VariantProps<typeof textVariants>;
const Text = forwardRef<HTMLHeadingElement, TextProps>(
({ tag, color, className, ...props }, ref) => {
const Component = tag;
return (
<Component
ref={ref}
className={cn(textVariants({ color, className }))}
{...props}
>
{props.children}
</Component>
);
},
);
Text.displayName = "Text";
const MText = motion.create(Text);
const AnimatedText: FC<TextProps & HTMLMotionProps<Tags>> = (props) => {
return (
<MText
variants={{
enter: {
y: 0,
opacity: 1,
},
from: {
y: 10,
opacity: 0,
},
}}
transition={ease.quint(0.8).out}
ref={props.ref}
{...props}
/>
);
};
export default AnimatedText;
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}