diff --git a/.cursor/rules/components.md b/.cursor/rules/components.md index 8610b8d7..224a166c 100644 --- a/.cursor/rules/components.md +++ b/.cursor/rules/components.md @@ -2,6 +2,48 @@ Custom Components allow you to fully customize the Admin Panel by swapping in your own React components. You can replace nearly every part of the interface or add entirely new functionality. +## Project Conventions (Teqo) + +These conventions apply to all first-party components we author. The only exception is `src/components/ui/*`, which is generated/managed by the shadcn CLI and kept verbatim (including its `export function` declarations and `import * as React from 'react'` style) so it stays easy to update from upstream. + +1. **Declare components with `export const`, not `export function`.** Use arrow functions: + + ```tsx + // ✅ First-party component + export const MyComponent = ({ title }: MyComponentProps) => { + return

{title}

+ } + + // ❌ Reserved for src/components/ui/* (shadcn) only + export function MyComponent({ title }: MyComponentProps) { + return

{title}

+ } + ``` + + For a generic component in a `.tsx` file, the `extends` clause keeps the type parameter unambiguous from JSX: + + ```tsx + export const FormField = ( + props: FormFieldProps, + ) => { + /* ... */ + } + ``` + +2. **Import React hooks by name; do not use the `React.*` namespace.** Import each hook (and other named React APIs) directly: + + ```tsx + // ✅ First-party component + import { useCallback, useMemo, useState } from 'react' + + const [open, setOpen] = useState(false) + + // ❌ Reserved for src/components/ui/* (shadcn) only + import * as React from 'react' + + const [open, setOpen] = React.useState(false) + ``` + ## Component Types There are four main types of Custom Components: diff --git a/package.json b/package.json index ec5926cc..05c7a79e 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "test:int": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts" }, "dependencies": { + "@base-ui/react": "^1.6.0", "@hookform/resolvers": "^5.2.2", "@payloadcms/db-postgres": "3.79.0", "@payloadcms/live-preview-react": "3.79.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b042645..a87e1eb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@base-ui/react': + specifier: ^1.6.0 + version: 1.6.0(@date-fns/tz@1.2.0)(@types/react@19.2.14)(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.71.1(react@19.2.4)) @@ -328,6 +331,10 @@ packages: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -340,6 +347,33 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@base-ui/react@1.6.0': + resolution: {integrity: sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@date-fns/tz': ^1.2.0 + '@types/react': ^17 || ^18 || ^19 + date-fns: ^4.0.0 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@date-fns/tz': + optional: true + '@types/react': + optional: true + date-fns: + optional: true + + '@base-ui/utils@0.3.1': + resolution: {integrity: sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@borewit/text-codec@0.2.1': resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} @@ -992,15 +1026,27 @@ packages: '@floating-ui/core@1.7.4': resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + '@floating-ui/dom@1.7.5': resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + '@floating-ui/react-dom@2.1.7': resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + '@floating-ui/react@0.27.17': resolution: {integrity: sha512-LGVZKHwmWGg6MRHjLLgsfyaX2y2aCNgnD1zT/E6B+/h+vxg+nIJUqHPAlTzsHDyqdgEpJ1Np5kxWuFEErXzoGg==} peerDependencies: @@ -1010,6 +1056,9 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + '@hono/node-server@1.19.11': resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} engines: {node: '>=18.14.1'} @@ -5498,6 +5547,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + resolve-dir@1.0.1: resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} engines: {node: '>=0.10.0'} @@ -6589,6 +6641,8 @@ snapshots: '@babel/runtime@7.28.6': {} + '@babel/runtime@7.29.7': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -6612,6 +6666,31 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@base-ui/react@1.6.0(@date-fns/tz@1.2.0)(@types/react@19.2.14)(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.29.7 + '@base-ui/utils': 0.3.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/utils': 0.2.12 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@date-fns/tz': 1.2.0 + '@types/react': 19.2.14 + date-fns: 4.1.0 + + '@base-ui/utils@0.3.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.29.7 + '@floating-ui/utils': 0.2.12 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + reselect: 5.2.0 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@borewit/text-codec@0.2.1': {} '@csstools/color-helpers@6.0.1': {} @@ -7074,17 +7153,32 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.10 + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + '@floating-ui/dom@1.7.5': dependencies: '@floating-ui/core': 1.7.4 '@floating-ui/utils': 0.2.10 + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + '@floating-ui/react-dom@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/dom': 1.7.5 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + '@floating-ui/react-dom@2.1.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.8.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + '@floating-ui/react@0.27.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -7095,6 +7189,8 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@floating-ui/utils@0.2.12': {} + '@hono/node-server@1.19.11(hono@4.12.7)': dependencies: hono: 4.12.7 @@ -11983,6 +12079,8 @@ snapshots: require-from-string@2.0.2: {} + reselect@5.2.0: {} + resolve-dir@1.0.1: dependencies: expand-tilde: 2.0.2 diff --git a/src/components/CitySelect.tsx b/src/components/CitySelect.tsx index 0595deef..83ff077a 100644 --- a/src/components/CitySelect.tsx +++ b/src/components/CitySelect.tsx @@ -1,33 +1,43 @@ -import { useFormContext, useWatch } from 'react-hook-form' -import { NativeSelect, NativeSelectOption } from './ui/native-select' import { CitiesByState } from '@/lib/cities' -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' +import { useFormContext, useWatch } from 'react-hook-form' +import { FormCombobox, type ComboboxOption } from './FormCombobox' interface CitySelectProps { placeholder?: string } export const CitySelect = ({ placeholder = 'Selecione uma cidade' }: CitySelectProps) => { - const { register, control, setValue } = useFormContext() + const { control, setValue } = useFormContext() const state: keyof typeof CitiesByState | undefined = useWatch({ control, name: 'state', }) - const cityOptions = state?.length ? CitiesByState[state] : [] + const options = useMemo( + () => + state + ? CitiesByState[state] + .map((city) => ({ value: city, label: city })) + .sort((a, b) => a.label.localeCompare(b.label, 'pt-BR')) + : [], + [state], + ) useEffect(() => { - setValue('city', undefined) - }, [state]) + setValue('city', '') + }, [state, setValue]) return ( - - {placeholder} - {cityOptions.map((city) => ( - - {city} - - ))} - + ) } diff --git a/src/components/FormCombobox.tsx b/src/components/FormCombobox.tsx new file mode 100644 index 00000000..4db0ffb1 --- /dev/null +++ b/src/components/FormCombobox.tsx @@ -0,0 +1,126 @@ +'use client' + +import { useCallback, useMemo, useState } from 'react' +import { type FieldPath, type FieldValues, useController, useFormContext } from 'react-hook-form' + +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, +} from '@/components/ui/combobox' +import { cn } from '@/lib/utils' + +export interface ComboboxOption { + value: string + label: string + keywords?: string[] +} + +interface FormComboboxProps { + name: FieldPath + options: ComboboxOption[] + id?: string + placeholder?: string + /** Minimum characters typed before the list of options is shown. */ + minChars?: number + disabled?: boolean + required?: boolean + emptyMessage?: string + className?: string +} + +const normalize = (value: string) => + value + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim() + +export const FormCombobox = ({ + name, + options, + id, + placeholder, + minChars = 3, + disabled, + required, + emptyMessage = 'Nenhum resultado encontrado.', + className, +}: FormComboboxProps) => { + const { control } = useFormContext() + const { + field, + fieldState: { invalid }, + } = useController({ name, control }) + + const [open, setOpen] = useState(false) + + const selectedItem = useMemo( + () => options.find((option) => option.value === field.value) ?? null, + [options, field.value], + ) + + const filter = useCallback( + (item: ComboboxOption, query: string) => { + const normalizedQuery = normalize(query) + if (normalizedQuery.length < minChars) { + return false + } + return [item.label, item.value, ...(item.keywords ?? [])].some((candidate) => + normalize(candidate).startsWith(normalizedQuery), + ) + }, + [minChars], + ) + + return ( + + items={options} + value={selectedItem} + onValueChange={(value) => field.onChange(value ? value.value : '')} + open={open} + onOpenChange={(nextOpen) => { + if (!nextOpen) { + setOpen(false) + } + }} + onInputValueChange={(inputValue, details) => { + setOpen(details.reason === 'input-change' && normalize(inputValue).length >= minChars) + }} + openOnInputClick={false} + filter={filter} + isItemEqualToValue={(a, b) => a?.value === b?.value} + disabled={disabled} + > + + + {emptyMessage} + + {(item: ComboboxOption) => ( + + {item.label} + + )} + + + + ) +} diff --git a/src/components/StateSelect.tsx b/src/components/StateSelect.tsx index e109525b..40c5d517 100644 --- a/src/components/StateSelect.tsx +++ b/src/components/StateSelect.tsx @@ -1,22 +1,24 @@ -import { useFormContext } from 'react-hook-form' -import { NativeSelect, NativeSelectOption } from './ui/native-select' -import { CitiesByState } from '@/lib/cities' +import { StateNames } from '@/lib/states' +import { FormCombobox, type ComboboxOption } from './FormCombobox' interface StateSelectProps { placeholder?: string } -export const StateSelect = ({ placeholder = 'Selecione um estado' }: StateSelectProps) => { - const { register } = useFormContext() +const stateOptions: ComboboxOption[] = Object.entries(StateNames) + .map(([code, name]) => ({ value: code, label: name, keywords: [code] })) + .sort((a, b) => a.label.localeCompare(b.label, 'pt-BR')) +export const StateSelect = ({ placeholder = 'Selecione um estado' }: StateSelectProps) => { return ( - - {placeholder} - {Object.keys(CitiesByState).map((state) => ( - - {state} - - ))} - + ) } diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx new file mode 100644 index 00000000..352afc33 --- /dev/null +++ b/src/components/ui/combobox.tsx @@ -0,0 +1,299 @@ +"use client" + +import * as React from "react" +import { Combobox as ComboboxPrimitive } from "@base-ui/react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from "@/components/ui/input-group" +import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react" + +const Combobox = ComboboxPrimitive.Root + +function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { + return +} + +function ComboboxTrigger({ + className, + children, + ...props +}: ComboboxPrimitive.Trigger.Props) { + return ( + + {children} + + + ) +} + +function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { + return ( + } + className={cn(className)} + {...props} + > + + + ) +} + +function ComboboxInput({ + className, + children, + disabled = false, + showTrigger = true, + showClear = false, + ...props +}: ComboboxPrimitive.Input.Props & { + showTrigger?: boolean + showClear?: boolean +}) { + return ( + + } + {...props} + /> + + {showTrigger && ( + + + + )} + {showClear && } + + {children} + + ) +} + +function ComboboxContent({ + className, + side = "bottom", + sideOffset = 6, + align = "start", + alignOffset = 0, + anchor, + ...props +}: ComboboxPrimitive.Popup.Props & + Pick< + ComboboxPrimitive.Positioner.Props, + "side" | "align" | "sideOffset" | "alignOffset" | "anchor" + >) { + return ( + + + + + + ) +} + +function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { + return ( + + ) +} + +function ComboboxItem({ + className, + children, + ...props +}: ComboboxPrimitive.Item.Props) { + return ( + + {children} + + } + > + + + + ) +} + +function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { + return ( + + ) +} + +function ComboboxLabel({ + className, + ...props +}: ComboboxPrimitive.GroupLabel.Props) { + return ( + + ) +} + +function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { + return ( + + ) +} + +function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { + return ( + + ) +} + +function ComboboxSeparator({ + className, + ...props +}: ComboboxPrimitive.Separator.Props) { + return ( + + ) +} + +function ComboboxChips({ + className, + ...props +}: React.ComponentPropsWithRef & + ComboboxPrimitive.Chips.Props) { + return ( + + ) +} + +function ComboboxChip({ + className, + children, + showRemove = true, + ...props +}: ComboboxPrimitive.Chip.Props & { + showRemove?: boolean +}) { + return ( + + {children} + {showRemove && ( + } + className="-ml-1 opacity-50 hover:opacity-100" + data-slot="combobox-chip-remove" + > + + + )} + + ) +} + +function ComboboxChipsInput({ + className, + ...props +}: ComboboxPrimitive.Input.Props) { + return ( + + ) +} + +function useComboboxAnchor() { + return React.useRef(null) +} + +export { + Combobox, + ComboboxInput, + ComboboxContent, + ComboboxList, + ComboboxItem, + ComboboxGroup, + ComboboxLabel, + ComboboxCollection, + ComboboxEmpty, + ComboboxSeparator, + ComboboxChips, + ComboboxChip, + ComboboxChipsInput, + ComboboxTrigger, + ComboboxValue, + useComboboxAnchor, +} diff --git a/src/components/ui/input-group.tsx b/src/components/ui/input-group.tsx new file mode 100644 index 00000000..256ba4bb --- /dev/null +++ b/src/components/ui/input-group.tsx @@ -0,0 +1,156 @@ +"use client" + +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" + +function InputGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", + className + )} + {...props} + /> + ) +} + +const inputGroupAddonVariants = cva( + "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", + { + variants: { + align: { + "inline-start": + "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]", + "inline-end": + "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]", + "block-start": + "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2", + "block-end": + "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2", + }, + }, + defaultVariants: { + align: "inline-start", + }, + } +) + +function InputGroupAddon({ + className, + align = "inline-start", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
{ + if ((e.target as HTMLElement).closest("button")) { + return + } + e.currentTarget.parentElement?.querySelector("input")?.focus() + }} + {...props} + /> + ) +} + +const inputGroupButtonVariants = cva( + "flex items-center gap-2 text-sm shadow-none", + { + variants: { + size: { + xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5", + sm: "", + "icon-xs": + "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0", + "icon-sm": "size-8 p-0 has-[>svg]:p-0", + }, + }, + defaultVariants: { + size: "xs", + }, + } +) + +function InputGroupButton({ + className, + type = "button", + variant = "ghost", + size = "xs", + ...props +}: Omit, "size"> & + VariantProps) { + return ( +