Merge pull request #61 from johndoe6345789/codex/split-ui-components-into-individual-files

refactor(ui): split ui primitive components into per-file modules
This commit is contained in:
2026-01-18 00:50:02 +00:00
committed by GitHub
133 changed files with 2903 additions and 2234 deletions

View File

@@ -1,157 +0,0 @@
"use client"
import { ComponentProps } from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-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-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -0,0 +1,21 @@
"use client"
import { ComponentProps } from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialogAction({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
export { AlertDialogAction }

View File

@@ -0,0 +1,21 @@
"use client"
import { ComponentProps } from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialogCancel({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export { AlertDialogCancel }

View File

@@ -0,0 +1,30 @@
"use client"
import { ComponentProps } from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { AlertDialogOverlay } from "./alert-dialog-overlay"
import { AlertDialogPortal } from "./alert-dialog-portal"
function AlertDialogContent({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
export { AlertDialogContent }

View File

@@ -0,0 +1,21 @@
"use client"
import { ComponentProps } from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
function AlertDialogDescription({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export { AlertDialogDescription }

View File

@@ -0,0 +1,23 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
function AlertDialogFooter({
className,
...props
}: ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
export { AlertDialogFooter }

View File

@@ -0,0 +1,20 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
function AlertDialogHeader({
className,
...props
}: ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
export { AlertDialogHeader }

View File

@@ -0,0 +1,24 @@
"use client"
import { ComponentProps } from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
function AlertDialogOverlay({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-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-50 bg-black/50",
className
)}
{...props}
/>
)
}
export { AlertDialogOverlay }

View File

@@ -0,0 +1,14 @@
"use client"
import { ComponentProps } from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
function AlertDialogPortal({
...props
}: ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
export { AlertDialogPortal }

View File

@@ -0,0 +1,21 @@
"use client"
import { ComponentProps } from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
function AlertDialogTitle({
className,
...props
}: ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
export { AlertDialogTitle }

View File

@@ -0,0 +1,14 @@
"use client"
import { ComponentProps } from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
function AlertDialogTrigger({
...props
}: ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
export { AlertDialogTrigger }

View File

@@ -0,0 +1,12 @@
"use client"
import { ComponentProps } from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
function AlertDialog({
...props
}: ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
export { AlertDialog }

View File

@@ -0,0 +1,11 @@
export { AlertDialog } from "./alert-dialog"
export { AlertDialogAction } from "./alert-dialog-action"
export { AlertDialogCancel } from "./alert-dialog-cancel"
export { AlertDialogContent } from "./alert-dialog-content"
export { AlertDialogDescription } from "./alert-dialog-description"
export { AlertDialogFooter } from "./alert-dialog-footer"
export { AlertDialogHeader } from "./alert-dialog-header"
export { AlertDialogOverlay } from "./alert-dialog-overlay"
export { AlertDialogPortal } from "./alert-dialog-portal"
export { AlertDialogTitle } from "./alert-dialog-title"
export { AlertDialogTrigger } from "./alert-dialog-trigger"

View File

@@ -1,242 +0,0 @@
"use client"
import { ComponentProps, createContext, useCallback, useContext, useEffect, useState, KeyboardEvent } from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import ArrowLeft from "lucide-react/dist/esm/icons/arrow-left"
import ArrowRight from "lucide-react/dist/esm/icons/arrow-right"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
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 = createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = 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
}: ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = useState(false)
const [canScrollNext, setCanScrollNext] = useState(false)
const onSelect = useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = useCallback(
(event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
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 }: 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 }: 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
}: 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
}: 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,
}

View File

@@ -0,0 +1,30 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
import { useCarousel } from "./use-carousel"
function CarouselContent({ className, ...props }: 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>
)
}
export { CarouselContent }

View File

@@ -0,0 +1,18 @@
import { createContext } from "react"
import type { UseEmblaCarouselType } from "embla-carousel-react"
import type { CarouselProps } from "./carousel-types"
type CarouselContextProps = {
carouselRef: UseEmblaCarouselType[0]
api: UseEmblaCarouselType[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = createContext<CarouselContextProps | null>(null)
export type { CarouselContextProps }
export { CarouselContext }

View File

@@ -0,0 +1,27 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
import { useCarousel } from "./use-carousel"
function CarouselItem({ className, ...props }: 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}
/>
)
}
export { CarouselItem }

View File

@@ -0,0 +1,41 @@
"use client"
import { ComponentProps } from "react"
import ArrowRight from "lucide-react/dist/esm/icons/arrow-right"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { useCarousel } from "./use-carousel"
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: 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 { CarouselNext }

View File

@@ -0,0 +1,41 @@
"use client"
import { ComponentProps } from "react"
import ArrowLeft from "lucide-react/dist/esm/icons/arrow-left"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { useCarousel } from "./use-carousel"
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: 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>
)
}
export { CarouselPrevious }

View File

@@ -0,0 +1,18 @@
import type { UseEmblaCarouselType } from "embla-carousel-react"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof import("embla-carousel-react").default>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
export type { CarouselApi, CarouselOptions, CarouselPlugin, CarouselProps }

View File

@@ -0,0 +1,109 @@
"use client"
import {
ComponentProps,
KeyboardEvent,
useCallback,
useEffect,
useState,
} from "react"
import useEmblaCarousel from "embla-carousel-react"
import { cn } from "@/lib/utils"
import { CarouselContext, type CarouselContextProps } from "./carousel-context"
import type { CarouselApi, CarouselProps } from "./carousel-types"
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = useState(false)
const [canScrollNext, setCanScrollNext] = useState(false)
const onSelect = useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = useCallback(
(event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
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,
} as CarouselContextProps
}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
export { Carousel }

View File

@@ -0,0 +1,6 @@
export { Carousel } from "./carousel"
export { CarouselContent } from "./carousel-content"
export { CarouselItem } from "./carousel-item"
export { CarouselNext } from "./carousel-next"
export { CarouselPrevious } from "./carousel-previous"
export { type CarouselApi } from "./carousel-types"

View File

@@ -0,0 +1,17 @@
"use client"
import { useContext } from "react"
import { CarouselContext } from "./carousel-context"
function useCarousel() {
const context = useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
export { useCarousel }

View File

@@ -1,351 +0,0 @@
import { ComponentProps, ComponentType, createContext, CSSProperties, ReactNode, useContext, useId, useMemo } from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: ReactNode
icon?: ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = createContext<ChartContextProps | null>(null)
function useChart() {
const context = useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
function ChartContainer({
id,
className,
children,
config,
...props
}: ComponentProps<"div"> & {
config: ChartConfig
children: ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}) {
const uniqueId = useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: ComponentProps<typeof RechartsPrimitive.Tooltip> &
ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
const { config } = useChart()
const tooltipLabel = useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
const ChartLegend = RechartsPrimitive.Legend
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

@@ -0,0 +1,16 @@
import { ComponentType, ReactNode } from "react"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: ReactNode
icon?: ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
export { THEMES }

View File

@@ -0,0 +1,45 @@
import { ComponentProps, useId } from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
import { ChartContext } from "./chart-context"
import { type ChartConfig } from "./chart-constants"
import { ChartStyle } from "./chart-style"
function ChartContainer({
id,
className,
children,
config,
...props
}: ComponentProps<"div"> & {
config: ChartConfig
children: ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}) {
const uniqueId = useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
}
export { ChartContainer }

View File

@@ -0,0 +1,12 @@
import { createContext } from "react"
import type { ChartConfig } from "./chart-constants"
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = createContext<ChartContextProps | null>(null)
export type { ChartContextProps }
export { ChartContext }

View File

@@ -0,0 +1,63 @@
import { ComponentProps } from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
import { getPayloadConfigFromPayload } from "./get-payload-config-from-payload"
import { useChart } from "./use-chart"
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
export { ChartLegendContent }

View File

@@ -0,0 +1,5 @@
import * as RechartsPrimitive from "recharts"
const ChartLegend = RechartsPrimitive.Legend
export { ChartLegend }

View File

@@ -0,0 +1,36 @@
import { THEMES, type ChartConfig } from "./chart-constants"
function ChartStyle({ id, config }: { id: string; config: ChartConfig }) {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
export { ChartStyle }

View File

@@ -0,0 +1,153 @@
import { ComponentProps, CSSProperties, useMemo } from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
import { getPayloadConfigFromPayload } from "./get-payload-config-from-payload"
import { useChart } from "./use-chart"
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: ComponentProps<typeof RechartsPrimitive.Tooltip> &
ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
const { config } = useChart()
const tooltipLabel = useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
export { ChartTooltipContent }

View File

@@ -0,0 +1,5 @@
import * as RechartsPrimitive from "recharts"
const ChartTooltip = RechartsPrimitive.Tooltip
export { ChartTooltip }

View File

@@ -0,0 +1,42 @@
import type { ChartConfig } from "./chart-constants"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export { getPayloadConfigFromPayload }

View File

@@ -0,0 +1,7 @@
export { ChartContainer } from "./chart-container"
export { ChartLegend } from "./chart-legend"
export { ChartLegendContent } from "./chart-legend-content"
export { ChartStyle } from "./chart-style"
export { ChartTooltip } from "./chart-tooltip"
export { ChartTooltipContent } from "./chart-tooltip-content"
export { type ChartConfig } from "./chart-constants"

View File

@@ -0,0 +1,15 @@
import { useContext } from "react"
import { ChartContext } from "./chart-context"
function useChart() {
const context = useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
export { useChart }

View File

@@ -1,177 +0,0 @@
"use client"
import { ComponentProps } from "react"
import { Command as CommandPrimitive } from "cmdk"
import SearchIcon from "lucide-react/dist/esm/icons/search"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
...props
}: ComponentProps<typeof Dialog> & {
title?: string
description?: string
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -0,0 +1,39 @@
"use client"
import { ComponentProps } from "react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Command } from "./command"
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
...props
}: ComponentProps<typeof Dialog> & {
title?: string
description?: string
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
export { CommandDialog }

View File

@@ -0,0 +1,18 @@
"use client"
import { ComponentProps } from "react"
import { Command as CommandPrimitive } from "cmdk"
function CommandEmpty({
...props
}: ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
export { CommandEmpty }

View File

@@ -0,0 +1,24 @@
"use client"
import { ComponentProps } from "react"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
function CommandGroup({
className,
...props
}: ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
export { CommandGroup }

View File

@@ -0,0 +1,31 @@
"use client"
import { ComponentProps } from "react"
import { Command as CommandPrimitive } from "cmdk"
import SearchIcon from "lucide-react/dist/esm/icons/search"
import { cn } from "@/lib/utils"
function CommandInput({
className,
...props
}: ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
export { CommandInput }

View File

@@ -0,0 +1,24 @@
"use client"
import { ComponentProps } from "react"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
function CommandItem({
className,
...props
}: ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
export { CommandItem }

View File

@@ -0,0 +1,24 @@
"use client"
import { ComponentProps } from "react"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
function CommandList({
className,
...props
}: ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
export { CommandList }

View File

@@ -0,0 +1,21 @@
"use client"
import { ComponentProps } from "react"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
function CommandSeparator({
className,
...props
}: ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
export { CommandSeparator }

View File

@@ -0,0 +1,23 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
function CommandShortcut({
className,
...props
}: ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export { CommandShortcut }

View File

@@ -0,0 +1,24 @@
"use client"
import { ComponentProps } from "react"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
function Command({
className,
...props
}: ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
export { Command }

View File

@@ -0,0 +1,9 @@
export { Command } from "./command"
export { CommandDialog } from "./command-dialog"
export { CommandEmpty } from "./command-empty"
export { CommandGroup } from "./command-group"
export { CommandInput } from "./command-input"
export { CommandItem } from "./command-item"
export { CommandList } from "./command-list"
export { CommandSeparator } from "./command-separator"
export { CommandShortcut } from "./command-shortcut"

View File

@@ -1,254 +0,0 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import CheckIcon from "lucide-react/dist/esm/icons/check";
import ChevronRightIcon from "lucide-react/dist/esm/icons/chevron-right"
import CircleIcon from "lucide-react/dist/esm/icons/circle"
import { cn } from "@/lib/utils"
function ContextMenu({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
function ContextMenuTrigger({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
)
}
function ContextMenuGroup({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
}
function ContextMenuPortal({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
}
function ContextMenuSub({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
}
function ContextMenuRadioGroup({
...props
}: ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
}
function ContextMenuSubContent({
className,
...props
}: ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
function ContextMenuContent({
className,
...props
}: ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
)
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
function ContextMenuRadioItem({
className,
children,
...props
}: ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
function ContextMenuLabel({
className,
inset,
...props
}: ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function ContextMenuSeparator({
className,
...props
}: ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function ContextMenuShortcut({
className,
...props
}: ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View File

@@ -0,0 +1,35 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import CheckIcon from "lucide-react/dist/esm/icons/check"
import { cn } from "@/lib/utils"
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
export { ContextMenuCheckboxItem }

View File

@@ -0,0 +1,26 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { cn } from "@/lib/utils"
function ContextMenuContent({
className,
...props
}: ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
)
}
export { ContextMenuContent }

View File

@@ -0,0 +1,14 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
function ContextMenuGroup({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
}
export { ContextMenuGroup }

View File

@@ -0,0 +1,31 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { cn } from "@/lib/utils"
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
export { ContextMenuItem }

View File

@@ -0,0 +1,28 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { cn } from "@/lib/utils"
function ContextMenuLabel({
className,
inset,
...props
}: ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
export { ContextMenuLabel }

View File

@@ -0,0 +1,14 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
function ContextMenuPortal({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
}
export { ContextMenuPortal }

View File

@@ -0,0 +1,17 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
function ContextMenuRadioGroup({
...props
}: ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
export { ContextMenuRadioGroup }

View File

@@ -0,0 +1,33 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import CircleIcon from "lucide-react/dist/esm/icons/circle"
import { cn } from "@/lib/utils"
function ContextMenuRadioItem({
className,
children,
...props
}: ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
export { ContextMenuRadioItem }

View File

@@ -0,0 +1,21 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { cn } from "@/lib/utils"
function ContextMenuSeparator({
className,
...props
}: ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
export { ContextMenuSeparator }

View File

@@ -0,0 +1,23 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
function ContextMenuShortcut({
className,
...props
}: ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export { ContextMenuShortcut }

View File

@@ -0,0 +1,24 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { cn } from "@/lib/utils"
function ContextMenuSubContent({
className,
...props
}: ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export { ContextMenuSubContent }

View File

@@ -0,0 +1,33 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import ChevronRightIcon from "lucide-react/dist/esm/icons/chevron-right"
import { cn } from "@/lib/utils"
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
}
export { ContextMenuSubTrigger }

View File

@@ -0,0 +1,12 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
function ContextMenuSub({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
}
export { ContextMenuSub }

View File

@@ -0,0 +1,14 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
function ContextMenuTrigger({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
)
}
export { ContextMenuTrigger }

View File

@@ -0,0 +1,12 @@
"use client"
import { ComponentProps } from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
function ContextMenu({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
export { ContextMenu }

View File

@@ -0,0 +1,15 @@
export { ContextMenu } from "./context-menu"
export { ContextMenuCheckboxItem } from "./context-menu-checkbox-item"
export { ContextMenuContent } from "./context-menu-content"
export { ContextMenuGroup } from "./context-menu-group"
export { ContextMenuItem } from "./context-menu-item"
export { ContextMenuLabel } from "./context-menu-label"
export { ContextMenuPortal } from "./context-menu-portal"
export { ContextMenuRadioGroup } from "./context-menu-radio-group"
export { ContextMenuRadioItem } from "./context-menu-radio-item"
export { ContextMenuSeparator } from "./context-menu-separator"
export { ContextMenuShortcut } from "./context-menu-shortcut"
export { ContextMenuSub } from "./context-menu-sub"
export { ContextMenuSubContent } from "./context-menu-sub-content"
export { ContextMenuSubTrigger } from "./context-menu-sub-trigger"
export { ContextMenuTrigger } from "./context-menu-trigger"

View File

@@ -1,259 +0,0 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import CheckIcon from "lucide-react/dist/esm/icons/check"
import ChevronRightIcon from "lucide-react/dist/esm/icons/chevron-right"
import CircleIcon from "lucide-react/dist/esm/icons/circle"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@@ -0,0 +1,35 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import CheckIcon from "lucide-react/dist/esm/icons/check"
import { cn } from "@/lib/utils"
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
export { DropdownMenuCheckboxItem }

View File

@@ -0,0 +1,28 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { cn } from "@/lib/utils"
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
export { DropdownMenuContent }

View File

@@ -0,0 +1,14 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
function DropdownMenuGroup({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
export { DropdownMenuGroup }

View File

@@ -0,0 +1,31 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { cn } from "@/lib/utils"
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
export { DropdownMenuItem }

View File

@@ -0,0 +1,28 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { cn } from "@/lib/utils"
function DropdownMenuLabel({
className,
inset,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
export { DropdownMenuLabel }

View File

@@ -0,0 +1,14 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
function DropdownMenuPortal({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
export { DropdownMenuPortal }

View File

@@ -0,0 +1,17 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
function DropdownMenuRadioGroup({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
export { DropdownMenuRadioGroup }

View File

@@ -0,0 +1,33 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import CircleIcon from "lucide-react/dist/esm/icons/circle"
import { cn } from "@/lib/utils"
function DropdownMenuRadioItem({
className,
children,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
export { DropdownMenuRadioItem }

View File

@@ -0,0 +1,21 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { cn } from "@/lib/utils"
function DropdownMenuSeparator({
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
export { DropdownMenuSeparator }

View File

@@ -0,0 +1,23 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
function DropdownMenuShortcut({
className,
...props
}: ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export { DropdownMenuShortcut }

View File

@@ -0,0 +1,24 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { cn } from "@/lib/utils"
function DropdownMenuSubContent({
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export { DropdownMenuSubContent }

View File

@@ -0,0 +1,33 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import ChevronRightIcon from "lucide-react/dist/esm/icons/chevron-right"
import { cn } from "@/lib/utils"
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
export { DropdownMenuSubTrigger }

View File

@@ -0,0 +1,12 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
function DropdownMenuSub({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
export { DropdownMenuSub }

View File

@@ -0,0 +1,17 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
function DropdownMenuTrigger({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
export { DropdownMenuTrigger }

View File

@@ -0,0 +1,12 @@
"use client"
import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
function DropdownMenu({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
export { DropdownMenu }

View File

@@ -0,0 +1,15 @@
export { DropdownMenu } from "./dropdown-menu"
export { DropdownMenuCheckboxItem } from "./dropdown-menu-checkbox-item"
export { DropdownMenuContent } from "./dropdown-menu-content"
export { DropdownMenuGroup } from "./dropdown-menu-group"
export { DropdownMenuItem } from "./dropdown-menu-item"
export { DropdownMenuLabel } from "./dropdown-menu-label"
export { DropdownMenuPortal } from "./dropdown-menu-portal"
export { DropdownMenuRadioGroup } from "./dropdown-menu-radio-group"
export { DropdownMenuRadioItem } from "./dropdown-menu-radio-item"
export { DropdownMenuSeparator } from "./dropdown-menu-separator"
export { DropdownMenuShortcut } from "./dropdown-menu-shortcut"
export { DropdownMenuSub } from "./dropdown-menu-sub"
export { DropdownMenuSubContent } from "./dropdown-menu-sub-content"
export { DropdownMenuSubTrigger } from "./dropdown-menu-sub-trigger"
export { DropdownMenuTrigger } from "./dropdown-menu-trigger"

View File

@@ -1,165 +0,0 @@
import { ComponentProps, createContext, useContext, useId } from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = 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 = useContext(FormFieldContext)
const itemContext = useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
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 = createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: ComponentProps<"div">) {
const id = useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@@ -0,0 +1,24 @@
import { ComponentProps } from "react"
import { Slot } from "@radix-ui/react-slot"
import { useFormField } from "./use-form-field"
function FormControl({ ...props }: ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
export { FormControl }

View File

@@ -0,0 +1,20 @@
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
import { useFormField } from "./use-form-field"
function FormDescription({ className, ...props }: ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export { FormDescription }

View File

@@ -0,0 +1,16 @@
import { createContext } from "react"
import type { FieldPath, FieldValues } from "react-hook-form"
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
export type { FormFieldContextValue }
export { FormFieldContext }

View File

@@ -0,0 +1,23 @@
import {
Controller,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { FormFieldContext } from "./form-field-context"
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>
)
}
export { FormField }

View File

@@ -0,0 +1,12 @@
import { createContext } from "react"
type FormItemContextValue = {
id: string
}
const FormItemContext = createContext<FormItemContextValue>(
{} as FormItemContextValue
)
export type { FormItemContextValue }
export { FormItemContext }

View File

@@ -0,0 +1,21 @@
import { ComponentProps, useId } from "react"
import { cn } from "@/lib/utils"
import { FormItemContext } from "./form-item-context"
function FormItem({ className, ...props }: ComponentProps<"div">) {
const id = useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
export { FormItem }

View File

@@ -0,0 +1,26 @@
import { ComponentProps } from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { useFormField } from "./use-form-field"
function FormLabel({
className,
...props
}: ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
export { FormLabel }

View File

@@ -0,0 +1,27 @@
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
import { useFormField } from "./use-form-field"
function FormMessage({ className, ...props }: ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export { FormMessage }

View File

@@ -0,0 +1,5 @@
import { FormProvider } from "react-hook-form"
const Form = FormProvider
export { Form }

View File

@@ -0,0 +1,8 @@
export { Form } from "./form"
export { FormControl } from "./form-control"
export { FormDescription } from "./form-description"
export { FormField } from "./form-field"
export { FormItem } from "./form-item"
export { FormLabel } from "./form-label"
export { FormMessage } from "./form-message"
export { useFormField } from "./use-form-field"

View File

@@ -0,0 +1,30 @@
import { useContext } from "react"
import { useFormContext, useFormState } from "react-hook-form"
import { FormFieldContext } from "./form-field-context"
import { FormItemContext } from "./form-item-context"
const useFormField = () => {
const fieldContext = useContext(FormFieldContext)
const itemContext = useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
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,
}
}
export { useFormField }

View File

@@ -1,276 +0,0 @@
import { ComponentProps } from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import CheckIcon from "lucide-react/dist/esm/icons/check"
import ChevronRightIcon from "lucide-react/dist/esm/icons/chevron-right"
import CircleIcon from "lucide-react/dist/esm/icons/circle"
import { cn } from "@/lib/utils"
function Menubar({
className,
...props
}: ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className
)}
{...props}
/>
)
}
function MenubarMenu({
...props
}: ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
}
function MenubarGroup({
...props
}: ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
}
function MenubarPortal({
...props
}: ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
}
function MenubarRadioGroup({
...props
}: ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
)
}
function MenubarTrigger({
className,
...props
}: ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className
)}
{...props}
/>
)
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in 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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</MenubarPortal>
)
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 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",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
)
}
function MenubarRadioItem({
className,
children,
...props
}: ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 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",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
)
}
function MenubarLabel({
className,
inset,
...props
}: ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function MenubarSeparator({
className,
...props
}: ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function MenubarShortcut({
className,
...props
}: ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function MenubarSub({
...props
}: ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
)
}
function MenubarSubContent({
className,
...props
}: ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
}

View File

@@ -0,0 +1,16 @@
export { Menubar } from "./menubar"
export { MenubarCheckboxItem } from "./menubar-checkbox-item"
export { MenubarContent } from "./menubar-content"
export { MenubarGroup } from "./menubar-group"
export { MenubarItem } from "./menubar-item"
export { MenubarLabel } from "./menubar-label"
export { MenubarMenu } from "./menubar-menu"
export { MenubarPortal } from "./menubar-portal"
export { MenubarRadioGroup } from "./menubar-radio-group"
export { MenubarRadioItem } from "./menubar-radio-item"
export { MenubarSeparator } from "./menubar-separator"
export { MenubarShortcut } from "./menubar-shortcut"
export { MenubarSub } from "./menubar-sub"
export { MenubarSubContent } from "./menubar-sub-content"
export { MenubarSubTrigger } from "./menubar-sub-trigger"
export { MenubarTrigger } from "./menubar-trigger"

View File

@@ -0,0 +1,33 @@
import { ComponentProps } from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import CheckIcon from "lucide-react/dist/esm/icons/check"
import { cn } from "@/lib/utils"
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 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",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
)
}
export { MenubarCheckboxItem }

View File

@@ -0,0 +1,32 @@
import { ComponentProps } from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { cn } from "@/lib/utils"
import { MenubarPortal } from "./menubar-portal"
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in 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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</MenubarPortal>
)
}
export { MenubarContent }

View File

@@ -0,0 +1,10 @@
import { ComponentProps } from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
function MenubarGroup({
...props
}: ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
}
export { MenubarGroup }

View File

@@ -0,0 +1,29 @@
import { ComponentProps } from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { cn } from "@/lib/utils"
function MenubarItem({
className,
inset,
variant = "default",
...props
}: ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
export { MenubarItem }

View File

@@ -0,0 +1,26 @@
import { ComponentProps } from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { cn } from "@/lib/utils"
function MenubarLabel({
className,
inset,
...props
}: ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
export { MenubarLabel }

View File

@@ -0,0 +1,10 @@
import { ComponentProps } from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
function MenubarMenu({
...props
}: ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
}
export { MenubarMenu }

Some files were not shown because too many files have changed in this diff Show More