initial commit
This commit is contained in:
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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}`;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user