diff --git a/docs/llms-full.txt b/docs/llms-full.txt index bcfc07f..fea60af 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -685,7 +685,7 @@ MIT A React checkout address form for Nigeria that accepts the 11-character NIPOST digital postcode (NDAPS), such as `FC-02-A09-DB-09`. It is built with [shadcn/ui](https://ui.shadcn.com) and [`ng-postcode-js`](https://www.npmjs.com/package/ng-postcode-js), and this folder is a Next.js app that uses it. [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FAdeniyikayodee%2Fng-postcode%2Ftree%2Fmain%2Ftemplates%2Fnigerian-address-form&project-name=nigerian-address-form) -[![Open in v0](https://v0.app/chat-static/button.svg)](https://v0.dev/chat/api/open?url=https%3A%2F%2Fadeniyikayodee.github.io%2Fng-postcode%2Fr%2Fnigerian-address-form.json) +[![Open in v0](https://v0.app/chat-static/button.svg)](https://v0.app/chat/api/open?url=https%3A%2F%2Fadeniyikayodee.github.io%2Fng-postcode%2Fr%2Fnigerian-address-form.json) ## Add it to your project diff --git a/docs/r/nigerian-address-form.json b/docs/r/nigerian-address-form.json index 93e85eb..5e6742b 100644 --- a/docs/r/nigerian-address-form.json +++ b/docs/r/nigerian-address-form.json @@ -7,16 +7,10 @@ "ng-postcode-js@^0.4.4", "zod@^4.6.5" ], - "registryDependencies": [ - "button", - "field", - "input", - "native-select" - ], "files": [ { "path": "components/nigerian-address-form.tsx", - "content": "\"use client\";\n\nimport { type FocusEvent, type FormEvent, useRef, useState, useSyncExternalStore, useTransition } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from \"@/components/ui/field\";\nimport { Input } from \"@/components/ui/input\";\nimport { NativeSelect, NativeSelectOption } from \"@/components/ui/native-select\";\nimport {\n type Address,\n type AddressErrors,\n type AddressInput,\n addressSchema,\n checkPostcode,\n EXAMPLE,\n fieldErrors,\n STATES,\n} from \"@/lib/nigerian-address\";\n\ntype Props = {\n /** Return field errors to show them, such as a postcode the server refused. */\n onSubmit: (address: Address) => Promise | AddressErrors | void;\n postcodeRequired?: boolean;\n submitLabel?: string;\n};\n\ntype Name = keyof AddressInput;\n\nconst noop = () => () => {};\n\nexport function NigerianAddressForm({ onSubmit, postcodeRequired = false, submitLabel = \"Continue\" }: Props) {\n const form = useRef(null);\n const [errors, setErrors] = useState({});\n const [suggestion, setSuggestion] = useState();\n const [failed, setFailed] = useState(false);\n const [pending, startTransition] = useTransition();\n // Until React runs, a click would submit the address as a plain GET.\n const ready = useSyncExternalStore(noop, () => true, () => false);\n\n function show(next: AddressErrors) {\n setErrors(next);\n const first = Object.keys(next)[0];\n if (first) form.current?.querySelector(`[name=\"${first}\"]`)?.focus();\n }\n\n function checkOnBlur(event: FocusEvent) {\n const input = event.currentTarget;\n if (!input.value.trim()) return;\n const check = checkPostcode(input.value);\n if (check.ok) {\n input.value = check.postcode;\n return;\n }\n setSuggestion(check.suggestion);\n setErrors((current) => ({ ...current, postcode: check.error }));\n }\n\n // Cleared on edit, not on blur: a message vanishing as the customer clicks\n // \"Place order\" moves the button out from under the pointer.\n function clearOnEdit(event: FormEvent) {\n const { name } = event.target as HTMLInputElement;\n if (name === \"postcode\") setSuggestion(undefined);\n setErrors((current) => {\n if (!(name in current)) return current;\n const { [name as Name]: _, ...rest } = current;\n return rest;\n });\n }\n\n function applySuggestion() {\n const input = form.current?.elements.namedItem(\"postcode\");\n if (input instanceof HTMLInputElement && suggestion) {\n input.value = suggestion;\n // The button that had focus is about to go away.\n input.focus();\n }\n setSuggestion(undefined);\n setErrors(({ postcode: _, ...rest }) => rest);\n }\n\n function submit(event: FormEvent) {\n event.preventDefault();\n setFailed(false);\n const result = addressSchema({ postcodeRequired }).safeParse(\n Object.fromEntries(new FormData(event.currentTarget)),\n );\n if (!result.success) return show(fieldErrors(result.error));\n setErrors({});\n startTransition(async () => {\n try {\n const refused = await onSubmit(result.data);\n if (refused) show(refused);\n } catch {\n setFailed(true);\n }\n });\n }\n\n function field(name: Name) {\n const error = errors[name];\n return {\n id: name,\n name,\n \"aria-invalid\": error ? true : undefined,\n \"aria-describedby\": error ? `${name}-error` : undefined,\n };\n }\n\n function error(name: Name) {\n const message = errors[name];\n if (!message) return null;\n // Keep the example code on one line.\n const [before, after] = message.split(EXAMPLE);\n return (\n \n {after === undefined ? message : <>{before}{EXAMPLE}{after}}\n \n );\n }\n\n return (\n
\n \n \n Full name\n \n {error(\"name\")}\n \n \n Phone number\n \n {error(\"phone\")}\n \n \n Street address\n \n {error(\"street\")}\n \n \n Town or city\n \n {error(\"city\")}\n \n \n Local government area (optional)\n \n {error(\"lga\")}\n \n \n State\n \n \n Choose a state\n \n {STATES.map((state) => (\n \n {state}\n \n ))}\n \n {error(\"state\")}\n \n \n Postcode{postcodeRequired ? \"\" : \" (optional)\"}\n \n \n The 11-character NIPOST postcode. Find yours at{\" \"}\n \n postcode.gov.ng\n \n .\n \n {error(\"postcode\")}\n {suggestion && (\n \n )}\n \n {failed && We could not send your address. Try again.}\n \n \n
\n );\n}\n", + "content": "\"use client\";\n\nimport {\n type FocusEvent,\n type FormEvent,\n type ReactNode,\n useRef,\n useState,\n useSyncExternalStore,\n useTransition,\n} from \"react\";\nimport {\n type Address,\n type AddressErrors,\n type AddressInput,\n addressSchema,\n checkPostcode,\n EXAMPLE,\n fieldErrors,\n STATES,\n} from \"@/lib/nigerian-address\";\n\ntype Props = {\n /** Return field errors to show them, such as a postcode the server refused. Without it the form only validates. */\n onSubmit?: (address: Address) => Promise | AddressErrors | void;\n postcodeRequired?: boolean;\n submitLabel?: string;\n};\n\ntype Name = keyof AddressInput;\n\nconst noop = () => () => {};\n\n// Plain elements styled with the shadcn theme, so the form needs no other component\n// and installs the same in every shadcn style and in v0.\nconst control =\n \"h-9 w-full rounded-md border border-input bg-transparent px-3 text-base outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30\";\nconst button =\n \"inline-flex h-9 items-center justify-center rounded-md px-4 text-sm font-medium outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50\";\n\nexport function NigerianAddressForm({ onSubmit, postcodeRequired = false, submitLabel = \"Continue\" }: Props) {\n const form = useRef(null);\n const [errors, setErrors] = useState({});\n const [suggestion, setSuggestion] = useState();\n const [failed, setFailed] = useState(false);\n const [pending, startTransition] = useTransition();\n // Until React runs, a click would submit the address as a plain GET.\n const ready = useSyncExternalStore(noop, () => true, () => false);\n\n function show(next: AddressErrors) {\n setErrors(next);\n const first = Object.keys(next)[0];\n if (first) form.current?.querySelector(`[name=\"${first}\"]`)?.focus();\n }\n\n function checkOnBlur(event: FocusEvent) {\n const input = event.currentTarget;\n if (!input.value.trim()) return;\n const check = checkPostcode(input.value);\n if (check.ok) {\n input.value = check.postcode;\n return;\n }\n setSuggestion(check.suggestion);\n setErrors((current) => ({ ...current, postcode: check.error }));\n }\n\n // Cleared on edit, not on blur: a message vanishing as the customer clicks\n // \"Place order\" moves the button out from under the pointer.\n function clearOnEdit(event: FormEvent) {\n const { name } = event.target as HTMLInputElement;\n if (name === \"postcode\") setSuggestion(undefined);\n setErrors((current) => {\n if (!(name in current)) return current;\n const { [name as Name]: _, ...rest } = current;\n return rest;\n });\n }\n\n function applySuggestion() {\n const input = form.current?.elements.namedItem(\"postcode\");\n if (input instanceof HTMLInputElement && suggestion) {\n input.value = suggestion;\n // The button that had focus is about to go away.\n input.focus();\n }\n setSuggestion(undefined);\n setErrors(({ postcode: _, ...rest }) => rest);\n }\n\n function submit(event: FormEvent) {\n event.preventDefault();\n setFailed(false);\n const result = addressSchema({ postcodeRequired }).safeParse(\n Object.fromEntries(new FormData(event.currentTarget)),\n );\n if (!result.success) return show(fieldErrors(result.error));\n setErrors({});\n startTransition(async () => {\n try {\n const refused = await onSubmit?.(result.data);\n if (refused) show(refused);\n } catch {\n setFailed(true);\n }\n });\n }\n\n function field(name: Name) {\n const error = errors[name];\n return {\n id: name,\n name,\n \"aria-invalid\": error ? true : undefined,\n \"aria-describedby\": error ? `${name}-error` : undefined,\n };\n }\n\n function error(name: Name) {\n const message = errors[name];\n if (!message) return null;\n // Keep the example code on one line.\n const [before, after] = message.split(EXAMPLE);\n return (\n

\n {after === undefined ? message : <>{before}{EXAMPLE}{after}}\n

\n );\n }\n\n function row(name: Name, label: string, input: ReactNode, extra?: ReactNode) {\n return (\n
\n \n {input}\n {extra}\n {error(name)}\n
\n );\n }\n\n return (\n
\n {row(\"name\", \"Full name\", )}\n {row(\n \"phone\",\n \"Phone number\",\n ,\n )}\n {row(\"street\", \"Street address\", )}\n {row(\"city\", \"Town or city\", )}\n {row(\"lga\", \"Local government area (optional)\", )}\n {row(\n \"state\",\n \"State\",\n ,\n )}\n {row(\n \"postcode\",\n `Postcode${postcodeRequired ? \"\" : \" (optional)\"}`,\n ,\n

\n The 11-character NIPOST postcode. Find yours at{\" \"}\n \n postcode.gov.ng\n \n .\n

,\n )}\n {suggestion && (\n \n )}\n {failed && (\n

\n We could not send your address. Try again.\n

\n )}\n \n \n );\n}\n\nexport default NigerianAddressForm;\n", "type": "registry:component" }, { diff --git a/docs/r/registry.json b/docs/r/registry.json index 739ef89..67a3fe8 100644 --- a/docs/r/registry.json +++ b/docs/r/registry.json @@ -8,15 +8,30 @@ "type": "registry:block", "title": "Nigerian address form", "description": "Checkout address form for Nigeria that accepts the 11-character NIPOST digital postcode (NDAPS), such as FC-02-A09-DB-09, checked offline with ng-postcode-js.", - "dependencies": ["ng-postcode-js@^0.4.4", "zod@^4.6.5"], - "registryDependencies": ["button", "field", "input", "native-select"], + "dependencies": [ + "ng-postcode-js@^0.4.4", + "zod@^4.6.5" + ], "files": [ - { "path": "components/nigerian-address-form.tsx", "type": "registry:component" }, - { "path": "lib/nigerian-address.ts", "type": "registry:lib" }, - { "path": "lib/nipost.ts", "type": "registry:lib" } + { + "path": "components/nigerian-address-form.tsx", + "type": "registry:component" + }, + { + "path": "lib/nigerian-address.ts", + "type": "registry:lib" + }, + { + "path": "lib/nipost.ts", + "type": "registry:lib" + } ], "docs": "Validate again on the server with addressSchema().safeParse. To confirm a code is assigned, call isAssigned from lib/nipost.ts with nipostClient(process.env.NG_POSTCODE_API_KEY).", - "categories": ["forms", "checkout", "address"] + "categories": [ + "forms", + "checkout", + "address" + ] } ] } diff --git a/templates/nigerian-address-form/README.md b/templates/nigerian-address-form/README.md index bddbd8a..fc10ad3 100644 --- a/templates/nigerian-address-form/README.md +++ b/templates/nigerian-address-form/README.md @@ -3,7 +3,7 @@ A React checkout address form for Nigeria that accepts the 11-character NIPOST digital postcode (NDAPS), such as `FC-02-A09-DB-09`. It is built with [shadcn/ui](https://ui.shadcn.com) and [`ng-postcode-js`](https://www.npmjs.com/package/ng-postcode-js), and this folder is a Next.js app that uses it. [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FAdeniyikayodee%2Fng-postcode%2Ftree%2Fmain%2Ftemplates%2Fnigerian-address-form&project-name=nigerian-address-form) -[![Open in v0](https://v0.app/chat-static/button.svg)](https://v0.dev/chat/api/open?url=https%3A%2F%2Fadeniyikayodee.github.io%2Fng-postcode%2Fr%2Fnigerian-address-form.json) +[![Open in v0](https://v0.app/chat-static/button.svg)](https://v0.app/chat/api/open?url=https%3A%2F%2Fadeniyikayodee.github.io%2Fng-postcode%2Fr%2Fnigerian-address-form.json) ## Add it to your project diff --git a/templates/nigerian-address-form/components/nigerian-address-form.test.tsx b/templates/nigerian-address-form/components/nigerian-address-form.test.tsx index 9625721..1357eca 100644 --- a/templates/nigerian-address-form/components/nigerian-address-form.test.tsx +++ b/templates/nigerian-address-form/components/nigerian-address-form.test.tsx @@ -1,7 +1,7 @@ import { cleanup, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, expect, it, vi } from "vitest"; -import { NigerianAddressForm } from "./nigerian-address-form"; +import Default, { NigerianAddressForm } from "./nigerian-address-form"; afterEach(cleanup); @@ -96,3 +96,10 @@ it("clears an error once the customer edits the field", async () => { await user.tab(); expect(screen.getByRole("alert").textContent).toMatch(/A postcode has 11/); }); + +it("works as a default import with no props", async () => { + render(); + const user = await fill("FC-02-A09-DB-09"); + await user.click(screen.getByRole("button", { name: "Continue" })); + expect(screen.queryByRole("alert")).toBeNull(); +}); diff --git a/templates/nigerian-address-form/components/nigerian-address-form.tsx b/templates/nigerian-address-form/components/nigerian-address-form.tsx index 62a494e..ff1f916 100644 --- a/templates/nigerian-address-form/components/nigerian-address-form.tsx +++ b/templates/nigerian-address-form/components/nigerian-address-form.tsx @@ -1,10 +1,14 @@ "use client"; -import { type FocusEvent, type FormEvent, useRef, useState, useSyncExternalStore, useTransition } from "react"; -import { Button } from "@/components/ui/button"; -import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"; -import { Input } from "@/components/ui/input"; -import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"; +import { + type FocusEvent, + type FormEvent, + type ReactNode, + useRef, + useState, + useSyncExternalStore, + useTransition, +} from "react"; import { type Address, type AddressErrors, @@ -17,8 +21,8 @@ import { } from "@/lib/nigerian-address"; type Props = { - /** Return field errors to show them, such as a postcode the server refused. */ - onSubmit: (address: Address) => Promise | AddressErrors | void; + /** Return field errors to show them, such as a postcode the server refused. Without it the form only validates. */ + onSubmit?: (address: Address) => Promise | AddressErrors | void; postcodeRequired?: boolean; submitLabel?: string; }; @@ -27,6 +31,13 @@ type Name = keyof AddressInput; const noop = () => () => {}; +// Plain elements styled with the shadcn theme, so the form needs no other component +// and installs the same in every shadcn style and in v0. +const control = + "h-9 w-full rounded-md border border-input bg-transparent px-3 text-base outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30"; +const button = + "inline-flex h-9 items-center justify-center rounded-md px-4 text-sm font-medium outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50"; + export function NigerianAddressForm({ onSubmit, postcodeRequired = false, submitLabel = "Continue" }: Props) { const form = useRef(null); const [errors, setErrors] = useState({}); @@ -87,7 +98,7 @@ export function NigerianAddressForm({ onSubmit, postcodeRequired = false, submit setErrors({}); startTransition(async () => { try { - const refused = await onSubmit(result.data); + const refused = await onSubmit?.(result.data); if (refused) show(refused); } catch { setFailed(true); @@ -111,85 +122,87 @@ export function NigerianAddressForm({ onSubmit, postcodeRequired = false, submit // Keep the example code on one line. const [before, after] = message.split(EXAMPLE); return ( - + + ); + } + + function row(name: Name, label: string, input: ReactNode, extra?: ReactNode) { + return ( +
+ + {input} + {extra} + {error(name)} +
); } return ( -
- - - Full name - - {error("name")} - - - Phone number - - {error("phone")} - - - Street address - - {error("street")} - - - Town or city - - {error("city")} - - - Local government area (optional) - - {error("lga")} - - - State - - - Choose a state - - {STATES.map((state) => ( - - {state} - - ))} - - {error("state")} - - - Postcode{postcodeRequired ? "" : " (optional)"} - - - The 11-character NIPOST postcode. Find yours at{" "} - - postcode.gov.ng - - . - - {error("postcode")} - {suggestion && ( - - )} - - {failed && We could not send your address. Try again.} - - + + {row("name", "Full name", )} + {row( + "phone", + "Phone number", + , + )} + {row("street", "Street address", )} + {row("city", "Town or city", )} + {row("lga", "Local government area (optional)", )} + {row( + "state", + "State", + , + )} + {row( + "postcode", + `Postcode${postcodeRequired ? "" : " (optional)"}`, + , +

+ The 11-character NIPOST postcode. Find yours at{" "} + + postcode.gov.ng + + . +

, + )} + {suggestion && ( + + )} + {failed && ( +

+ We could not send your address. Try again. +

+ )} +
); } + +export default NigerianAddressForm; diff --git a/templates/nigerian-address-form/components/ui/button.tsx b/templates/nigerian-address-form/components/ui/button.tsx deleted file mode 100644 index 997e0c7..0000000 --- a/templates/nigerian-address-form/components/ui/button.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Button as ButtonPrimitive } from "@base-ui/react/button" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "cn" - -const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", - outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", - destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - icon: "size-8", - "icon-xs": - "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", - "icon-sm": - "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", - "icon-lg": "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -function Button({ - className, - variant = "default", - size = "default", - ...props -}: ButtonPrimitive.Props & VariantProps) { - return ( - - ) -} - -export { Button, buttonVariants } diff --git a/templates/nigerian-address-form/components/ui/field.tsx b/templates/nigerian-address-form/components/ui/field.tsx deleted file mode 100644 index 8ae7f29..0000000 --- a/templates/nigerian-address-form/components/ui/field.tsx +++ /dev/null @@ -1,238 +0,0 @@ -"use client" - -import { useMemo } from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "cn" - -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", - 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( - "group/field flex w-full gap-2 data-[invalid=true]:text-destructive", - { - variants: { - orientation: { - vertical: "flex-col *:w-full [&>.sr-only]:w-auto", - horizontal: - "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", - responsive: - "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @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 ( -