diff --git a/components/docs/interactive-examples.tsx b/components/docs/interactive-examples.tsx deleted file mode 100644 index 1c787de..0000000 --- a/components/docs/interactive-examples.tsx +++ /dev/null @@ -1,136 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { AlertDialogProvider, useAlert } from "@/registry/radix-nova/alert-provider"; -import { Button } from "@/registry/radix-nova/button"; -import { ConfirmDialogProvider, useConfirm } from "@/registry/radix-nova/confirm-provider"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/registry/radix-nova/dialog"; -import { LoadingButton } from "@/registry/radix-nova/loading-button"; - -const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -function AlertExampleContent() { - const alert = useAlert(); - const [status, setStatus] = useState("No alert shown yet."); - - const openAlert = async () => { - await alert({ - title: "Heads up", - description: "This is an interactive alert demo.", - buttonLabel: "Understood", - }); - setStatus("Alert dismissed."); - }; - - return ( -
- -

{status}

-
- ); -} - -function ConfirmExampleContent() { - const confirm = useConfirm(); - const [status, setStatus] = useState("No decision yet."); - - const openConfirm = async () => { - const isConfirmed = await confirm({ - title: "Delete item?", - description: "This action cannot be undone.", - confirmButtonText: "Delete", - cancelButtonText: "Cancel", - variant: "destructive", - requireConfirmationInput: { - confirmTerm: "DELETE", - hint: "Type DELETE to enable the button.", - }, - }); - setStatus(isConfirmed ? "Confirmed." : "Cancelled."); - }; - - return ( -
- -

{status}

-
- ); -} - -export function AlertDialogInteractiveExample() { - return ( - - - - ); -} - -export function ButtonInteractiveExample() { - return ( -
- - - -
- ); -} - -export function ConfirmDialogInteractiveExample() { - return ( - - - - ); -} - -export function DialogInteractiveExample() { - return ( - - - - - - - Profile updated - - This example uses custom close labels and the footer close button. - - - - - - ); -} - -export function LoadingButtonInteractiveExample() { - const [isLoading, setIsLoading] = useState(false); - const [count, setCount] = useState(0); - - const triggerLoading = async () => { - setIsLoading(true); - await wait(1200); - setIsLoading(false); - setCount((prev) => prev + 1); - }; - - return ( -
- void triggerLoading()}> - {isLoading ? "Submitting..." : "Submit"} - -

Completed: {count}

-
- ); -} diff --git a/components/docs/interactive-examples/alert-dialog.tsx b/components/docs/interactive-examples/alert-dialog.tsx new file mode 100644 index 0000000..374aedb --- /dev/null +++ b/components/docs/interactive-examples/alert-dialog.tsx @@ -0,0 +1,36 @@ +"use client" + +import { useState } from "react" +import { AlertDialogProvider, useAlert } from "@/registry/radix-nova/alert-provider" +import { Button } from "@/registry/radix-nova/button" + +function AlertExampleContent() { + const alert = useAlert() + const [status, setStatus] = useState("No alert shown yet.") + + const openAlert = async () => { + await alert({ + title: "Heads up", + description: "This is an interactive alert demo.", + buttonLabel: "Understood", + }) + setStatus("Alert dismissed.") + } + + return ( +
+ +

{status}

+
+ ) +} + +export function AlertDialogInteractiveExample() { + return ( + + + + ) +} diff --git a/components/docs/interactive-examples/button.tsx b/components/docs/interactive-examples/button.tsx new file mode 100644 index 0000000..cb67dfc --- /dev/null +++ b/components/docs/interactive-examples/button.tsx @@ -0,0 +1,11 @@ +import { Button } from "@/registry/radix-nova/button" + +export function ButtonInteractiveExample() { + return ( +
+ + + +
+ ) +} diff --git a/components/docs/interactive-examples/confirm-dialog.tsx b/components/docs/interactive-examples/confirm-dialog.tsx new file mode 100644 index 0000000..d50e2e7 --- /dev/null +++ b/components/docs/interactive-examples/confirm-dialog.tsx @@ -0,0 +1,42 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/registry/radix-nova/button" +import { ConfirmDialogProvider, useConfirm } from "@/registry/radix-nova/confirm-provider" + +function ConfirmExampleContent() { + const confirm = useConfirm() + const [status, setStatus] = useState("No decision yet.") + + const openConfirm = async () => { + const isConfirmed = await confirm({ + title: "Delete item?", + description: "This action cannot be undone.", + confirmButtonText: "Delete", + cancelButtonText: "Cancel", + variant: "destructive", + requireConfirmationInput: { + confirmTerm: "DELETE", + hint: "Type DELETE to enable the button.", + }, + }) + setStatus(isConfirmed ? "Confirmed." : "Cancelled.") + } + + return ( +
+ +

{status}

+
+ ) +} + +export function ConfirmDialogInteractiveExample() { + return ( + + + + ) +} diff --git a/components/docs/interactive-examples/dialog.tsx b/components/docs/interactive-examples/dialog.tsx new file mode 100644 index 0000000..3552cd4 --- /dev/null +++ b/components/docs/interactive-examples/dialog.tsx @@ -0,0 +1,29 @@ +import { Button } from "@/registry/radix-nova/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/registry/radix-nova/dialog" + +export function DialogInteractiveExample() { + return ( + + + + + + + Profile updated + + This example uses custom close labels and the footer close button. + + + + + + ) +} diff --git a/components/docs/interactive-examples/filepicker-dropzone.tsx b/components/docs/interactive-examples/filepicker-dropzone.tsx new file mode 100644 index 0000000..546a40e --- /dev/null +++ b/components/docs/interactive-examples/filepicker-dropzone.tsx @@ -0,0 +1,31 @@ +"use client" + +import { useState } from "react" +import { FilepickerDropzone } from "@/registry/radix-nova/filepicker-dropzone" + +export function FilepickerDropzoneInteractiveExample() { + const [files, setFiles] = useState([]) + + return ( +
+ { + setFiles(selectedFiles.map((file) => file.name)) + }} + /> +

+ {files.length > 0 ? `${files.length} file(s): ${files.join(", ")}` : "No files selected yet."} +

+
+ ) +} diff --git a/components/docs/interactive-examples/index.ts b/components/docs/interactive-examples/index.ts new file mode 100644 index 0000000..a1e873e --- /dev/null +++ b/components/docs/interactive-examples/index.ts @@ -0,0 +1,6 @@ +export { AlertDialogInteractiveExample } from "./alert-dialog" +export { ButtonInteractiveExample } from "./button" +export { ConfirmDialogInteractiveExample } from "./confirm-dialog" +export { DialogInteractiveExample } from "./dialog" +export { FilepickerDropzoneInteractiveExample } from "./filepicker-dropzone" +export { LoadingButtonInteractiveExample } from "./loading-button" diff --git a/components/docs/interactive-examples/loading-button.tsx b/components/docs/interactive-examples/loading-button.tsx new file mode 100644 index 0000000..9586608 --- /dev/null +++ b/components/docs/interactive-examples/loading-button.tsx @@ -0,0 +1,27 @@ +"use client" + +import { useState } from "react" +import { LoadingButton } from "@/registry/radix-nova/loading-button" + +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + +export function LoadingButtonInteractiveExample() { + const [isLoading, setIsLoading] = useState(false) + const [count, setCount] = useState(0) + + const triggerLoading = async () => { + setIsLoading(true) + await wait(1200) + setIsLoading(false) + setCount((prev) => prev + 1) + } + + return ( +
+ void triggerLoading()}> + {isLoading ? "Submitting..." : "Submit"} + +

Completed: {count}

+
+ ) +} diff --git a/components/ui/field.tsx b/components/ui/field.tsx new file mode 100644 index 0000000..52a4492 --- /dev/null +++ b/components/ui/field.tsx @@ -0,0 +1,227 @@ +"use client" + +import { useMemo } from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col", className)} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +const fieldVariants = cva("data-[invalid=true]:text-destructive gap-2 group/field flex w-full", { + variants: { + orientation: { + vertical: + "flex-col *:w-full [&>.sr-only]:w-auto", + horizontal: + "flex-row items-center *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + responsive: + "flex-col *:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:*:data-[slot=field-label]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + }, + }, + defaultVariants: { + orientation: "vertical", + }, +}) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +