From 18e4ad227e7021ee1f90dafe5f58d1a044857cf2 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:14:56 +0200 Subject: [PATCH 01/17] chore: add shadcn input-group components (#44282) ## Screenshots On a number input with units: image focused state: image On a textarea: image --- apps/design-system/__registry__/index.tsx | 22 +++ .../content/docs/fragments/data-input.mdx | 4 + ...-input-with-reveal-copy-editable-empty.tsx | 5 + .../data-input-with-reveal-copy-editable.tsx | 5 + .../example/form-patterns-pagelayout.tsx | 46 ++++- .../example/form-patterns-sidepanel.tsx | 16 +- .../default/example/page-layout-settings.tsx | 37 ++-- apps/design-system/registry/examples.ts | 12 ++ .../Auth/AuthProvidersForm/FormField.tsx | 49 +---- .../Auth/AuthProvidersFormValidation.tsx | 2 +- .../CreateOrUpdateCustomProviderSheet.tsx | 14 +- .../MfaAuthSettingsForm.tsx | 39 ++-- .../Auth/PerformanceSettingsForm.tsx | 35 ++-- .../ProtectionAuthSettingsForm.tsx | 40 +--- .../interfaces/Auth/RateLimits/RateLimits.tsx | 83 +++++--- .../SessionsAuthSettingsForm.tsx | 48 +++-- .../interfaces/Auth/SmtpForm/SmtpForm.tsx | 38 ++-- .../DestinationForm/AdvancedSettings.tsx | 51 +++-- .../DiskManagement/fields/AutoScaleFields.tsx | 186 ++++++++---------- .../DiskManagement/fields/DiskSizeField.tsx | 120 ++++++----- .../DiskManagement/fields/IOPSField.tsx | 58 +++--- .../DiskManagement/fields/ThroughputField.tsx | 67 +++---- .../DiskManagement/ui/InputPostTab.tsx | 21 -- .../DiskManagement/ui/InputResetButton.tsx | 30 --- .../OrganizationDetailsForm.tsx | 28 ++- .../interfaces/Realtime/RealtimeSettings.tsx | 62 +++--- .../Settings/API/PostgrestConfig.tsx | 26 ++- .../ConnectionPooling/ConnectionPooling.tsx | 49 +++-- .../Database/DiskSizeConfigurationModal.tsx | 15 +- packages/ui-patterns/src/DataInputs/Input.tsx | 45 +++-- packages/ui/index.tsx | 4 +- .../src/components/PrePostTab/PrePostTab.tsx | 37 ---- .../ui/src/components/PrePostTab/index.tsx | 2 - .../src/components/shadcn/ui/input-group.tsx | 159 +++++++++++++++ .../ui/src/components/shadcn/ui/input.tsx | 2 +- .../ui/src/components/shadcn/ui/textarea.tsx | 2 +- 36 files changed, 825 insertions(+), 634 deletions(-) create mode 100644 apps/design-system/registry/default/example/data-input-with-reveal-copy-editable-empty.tsx create mode 100644 apps/design-system/registry/default/example/data-input-with-reveal-copy-editable.tsx delete mode 100644 apps/studio/components/interfaces/DiskManagement/ui/InputPostTab.tsx delete mode 100644 apps/studio/components/interfaces/DiskManagement/ui/InputResetButton.tsx delete mode 100644 packages/ui/src/components/PrePostTab/PrePostTab.tsx delete mode 100644 packages/ui/src/components/PrePostTab/index.tsx create mode 100644 packages/ui/src/components/shadcn/ui/input-group.tsx diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index 8f423322ea9e6..f6ba8a7f0cd39 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -731,6 +731,28 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "data-input-with-reveal-copy-editable": { + name: "data-input-with-reveal-copy-editable", + type: "components:example", + registryDependencies: ["data-input"], + component: React.lazy(() => import("@/registry/default/example/data-input-with-reveal-copy-editable")), + source: "", + files: ["registry/default/example/data-input-with-reveal-copy-editable.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "data-input-with-reveal-copy-editable-empty": { + name: "data-input-with-reveal-copy-editable-empty", + type: "components:example", + registryDependencies: ["data-input"], + component: React.lazy(() => import("@/registry/default/example/data-input-with-reveal-copy-editable-empty")), + source: "", + files: ["registry/default/example/data-input-with-reveal-copy-editable-empty.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "date-picker-demo": { name: "date-picker-demo", type: "components:example", diff --git a/apps/design-system/content/docs/fragments/data-input.mdx b/apps/design-system/content/docs/fragments/data-input.mdx index 040c3e9dcc5ee..3badd62188389 100644 --- a/apps/design-system/content/docs/fragments/data-input.mdx +++ b/apps/design-system/content/docs/fragments/data-input.mdx @@ -22,6 +22,10 @@ Inputs with sensitive values can be both revealed _and_ copied, but only in succ +Inputs can be edited without revealing their value. + + + You can also partially truncate the value by overriding the placeholder value. Consider if the value needs to be revealed in the first place, as only copying is sufficient in most cases. diff --git a/apps/design-system/registry/default/example/data-input-with-reveal-copy-editable-empty.tsx b/apps/design-system/registry/default/example/data-input-with-reveal-copy-editable-empty.tsx new file mode 100644 index 0000000000000..dbee0094e3b81 --- /dev/null +++ b/apps/design-system/registry/default/example/data-input-with-reveal-copy-editable-empty.tsx @@ -0,0 +1,5 @@ +import { Input } from 'ui-patterns/DataInputs/Input' + +export default function DataInputWithRevealCopy() { + return +} diff --git a/apps/design-system/registry/default/example/data-input-with-reveal-copy-editable.tsx b/apps/design-system/registry/default/example/data-input-with-reveal-copy-editable.tsx new file mode 100644 index 0000000000000..3af57f3e55254 --- /dev/null +++ b/apps/design-system/registry/default/example/data-input-with-reveal-copy-editable.tsx @@ -0,0 +1,5 @@ +import { Input } from 'ui-patterns/DataInputs/Input' + +export default function DataInputWithRevealCopy() { + return +} diff --git a/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx b/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx index 49f00b4bee9a1..f1b6cc5ea68f7 100644 --- a/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx +++ b/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx @@ -14,10 +14,14 @@ import { FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_, + InputGroup, + InputGroupAddon, + InputGroupInput, + InputGroupText, + InputGroupTextarea, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, - PrePostTab, RadioGroupStacked, RadioGroupStackedItem, Select_Shadcn_, @@ -219,9 +223,12 @@ export default function FormPatternsPageLayout() { description="Input with additional unit label" > - - - + + + + MB + + )} @@ -252,6 +259,35 @@ export default function FormPatternsPageLayout() { /> + {/* Textarea with addon */} + + ( + + + + + + 120 characters left + + + + + )} + /> + + {/* Icon Upload */} - - )} - - ) -} diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDetailsForm.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDetailsForm.tsx index 7f5257ed41b1a..7a4326e80b5f9 100644 --- a/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDetailsForm.tsx +++ b/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDetailsForm.tsx @@ -1,18 +1,15 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' -import { useEffect } from 'react' -import { useForm } from 'react-hook-form' -import { toast } from 'sonner' -import * as z from 'zod' - import { useParams } from 'common' -import CopyButton from 'components/ui/CopyButton' import { FormActions } from 'components/ui/Forms/FormActions' import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useEffect } from 'react' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' import type { ResponseError } from 'types' import { Card, @@ -21,10 +18,11 @@ import { Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, - Input_Shadcn_ as Input, - PrePostTab, + Input_Shadcn_, } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import * as z from 'zod' const OrgDetailsSchema = z.object({ name: z.string().min(1, 'Organization name is required'), @@ -92,7 +90,10 @@ export const OrganizationDetailsForm = () => { render={({ field }) => ( - + )} @@ -100,14 +101,7 @@ export const OrganizationDetailsForm = () => { - - } - > - - + diff --git a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx index f235598abe9ca..9e7e0b367f841 100644 --- a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx +++ b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx @@ -1,11 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import Link from 'next/link' -import { useState } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' -import { toast } from 'sonner' -import * as z from 'zod' - import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { ToggleSpendCapButton } from 'components/ui/ToggleSpendCapButton' @@ -20,6 +14,10 @@ import { import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import Link from 'next/link' +import { useState } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' import { Button, Card, @@ -29,13 +27,16 @@ import { FormControl_Shadcn_, FormField_Shadcn_, FormMessage_Shadcn_, - Input_Shadcn_, - PrePostTab, + InputGroup, + InputGroupAddon, + InputGroupInput, + InputGroupText, Switch, } from 'ui' import { Admonition } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import * as z from 'zod' const formId = 'realtime-configuration-form' @@ -275,14 +276,17 @@ export const RealtimeSettings = () => { description="Realtime Authorization uses this database pool to check client access" > - - + + connections + + - + {!!maxConn && field.value > maxConn.maxConnections * 0.5 && ( @@ -308,14 +312,17 @@ export const RealtimeSettings = () => { description="Sets maximum number of concurrent clients that can connect to your Realtime service" > - - + + clients + + - + )} @@ -332,14 +339,17 @@ export const RealtimeSettings = () => { description="Sets maximum number of events per second that can be sent to your Realtime service" > - - + + events/s + + - + )} @@ -383,14 +393,17 @@ export const RealtimeSettings = () => { description="Sets maximum number of presence events per second that can be sent to your Realtime service" > - - + + events/s + + - + )} @@ -434,14 +447,17 @@ export const RealtimeSettings = () => { description="Sets maximum number of payload size in KB that can be sent to your Realtime service" > - - + + KB + + - + )} diff --git a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx index 89710a7cb0685..792790537e508 100644 --- a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx +++ b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx @@ -16,8 +16,10 @@ import { FormControl_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, - Input_Shadcn_, - PrePostTab, + InputGroup, + InputGroupAddon, + InputGroupInput, + InputGroupText, Skeleton, Switch, useWatch_Shadcn_, @@ -51,8 +53,8 @@ import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useDataApiGrantTogglesEnabled } from '@/hooks/misc/useDataApiGrantTogglesEnabled' import useLatest from '@/hooks/misc/useLatest' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { IS_PLATFORM } from '@/lib/constants' import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas' +import { IS_PLATFORM } from '@/lib/constants' import { noop } from '@/lib/void' import type { ResponseError } from '@/types' @@ -608,15 +610,18 @@ export const PostgrestConfig = () => { description="The maximum number of rows returned from a view, table, or stored procedure. Limits payload size for accidental or malicious requests." > - - + + rows + + field.onChange(Number(e.target.value))} /> - + @@ -635,8 +640,11 @@ export const PostgrestConfig = () => { description="Number of maximum connections to keep open in the Data API server's database pool. Unset to let it be configured automatically based on compute size." > - - + + connections + + { } value={field.value === null ? '' : field.value} /> - + diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx index e147f12d604fd..09ffb3fc8cfe1 100644 --- a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx +++ b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx @@ -1,14 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { capitalize } from 'lodash' -import { Fragment, useEffect } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' -import { toast } from 'sonner' -import z from 'zod' - import { useParams } from 'common' import AlertError from 'components/ui/AlertError' -import { Button } from 'ui' import { DocsButton } from 'components/ui/DocsButton' import { setValueAsNullableNumber } from 'components/ui/Forms/Form.constants' import { FormActions } from 'components/ui/Forms/FormActions' @@ -22,19 +15,28 @@ import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' +import { capitalize } from 'lodash' +import Link from 'next/link' +import { Fragment, useEffect } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' import { + Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, - Alert_Shadcn_, Badge, - cn, + Button, + Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, - Form_Shadcn_, - Input_Shadcn_, - PrePostTab, + InputGroup, + InputGroupAddon, + InputGroupInput, + InputGroupText, Separator, } from 'ui' +import { Admonition } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { PageSection, PageSectionAside, @@ -43,11 +45,10 @@ import { PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' -import { Admonition } from 'ui-patterns' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' +import z from 'zod' + import { POOLING_OPTIMIZATIONS } from './ConnectionPooling.constants' -import Link from 'next/link' const formId = 'pooling-configuration-form' @@ -258,8 +259,11 @@ export const ConnectionPooling = () => { className="[&>div]:md:w-1/2 [&>div]:xl:w-2/5 [&>div>div]:w-full [&>div>div>div]:min-w-100" > - - + + connections + + { setValueAs: setValueAsNullableNumber, })} /> - + {!!maxConnData && (default_pool_size ?? 15) > maxConnData.maxConnections * 0.8 && ( @@ -314,8 +318,11 @@ export const ConnectionPooling = () => { } > - - + + clients + + { setValueAs: setValueAsNullableNumber, })} /> - + )} diff --git a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx index 2e571c8e87812..19498ff0c6a99 100644 --- a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx @@ -23,9 +23,11 @@ import { FormControl_Shadcn_, FormField_Shadcn_, InfoIcon, - Input_Shadcn_, + InputGroup, + InputGroupAddon, + InputGroupInput, + InputGroupText, Modal, - PrePostTab, WarningIcon, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -192,16 +194,19 @@ const DiskSizeConfigurationModal = ({ layout="vertical" label="New disk size" > - + + + GB + - field.onChange(Number(e.target.value))} /> - + )} /> diff --git a/packages/ui-patterns/src/DataInputs/Input.tsx b/packages/ui-patterns/src/DataInputs/Input.tsx index 3c98d52308c8f..4200f05e70caa 100644 --- a/packages/ui-patterns/src/DataInputs/Input.tsx +++ b/packages/ui-patterns/src/DataInputs/Input.tsx @@ -4,15 +4,21 @@ import React, { ComponentPropsWithoutRef, ElementRef, forwardRef, + useMemo, useState, } from 'react' -import { Button, cn, copyToClipboard, Input_Shadcn_ } from 'ui' +import { + Button, + cn, + copyToClipboard, + Input_Shadcn_, + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from 'ui' import styleHandler from 'ui/src/lib/theme/styleHandler' -import InputIconContainer from '../form/Layout/InputIconContainer' - -export const HIDDEN_PLACEHOLDER = '**** **** **** ****' - export interface Props extends Omit, 'onCopy'> { copy?: boolean showCopyOnHover?: boolean @@ -68,21 +74,26 @@ const Input = forwardRef< if (icon) inputClasses.push(__styles.with_icon[size ?? 'small']) return ( -
- + event.target.select()} {...props} size={size} onCopy={onCopy} - value={reveal && hidden ? HIDDEN_PLACEHOLDER : props.value} - disabled={reveal && hidden ? true : props.disabled} + type={reveal && hidden ? 'password' : props.type} + disabled={props.disabled} className={cn(...inputClasses, props.className)} + data-1p-ignore // 1Password + data-lpignore="true" // LastPass + data-form-type="other" // Dashlane + data-bwignore // Bitwarden /> - {icon && } + {icon && {icon}} {copy || actions ? ( -
+ {copy && !(reveal && hidden) ? ( - + ) : null} {reveal && hidden ? ( - + ) : null} {actions && actions} -
+ ) : null} -
+ ) } ) diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index c746f0007898e..3207955f943bb 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -46,8 +46,6 @@ export * from './src/components/Toggle' export * from './src/components/Form' export * from './src/components/ExpandingTextArea' -export * from './src/components/PrePostTab' - // layout export * from './src/components/LoadingLine' @@ -202,6 +200,8 @@ export { TextArea as TextArea_Shadcn_ } from './src/components/shadcn/ui/text-ar export { Label as Label_Shadcn_ } from './src/components/shadcn/ui/label' +export * from './src/components/shadcn/ui/input-group' + export * from './src/components/shadcn/ui/switch' export { Checkbox as Checkbox_Shadcn_ } from './src/components/shadcn/ui/checkbox' diff --git a/packages/ui/src/components/PrePostTab/PrePostTab.tsx b/packages/ui/src/components/PrePostTab/PrePostTab.tsx deleted file mode 100644 index dc85d07137525..0000000000000 --- a/packages/ui/src/components/PrePostTab/PrePostTab.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { forwardRef, HTMLAttributes, PropsWithChildren, ReactNode } from 'react' - -import { cn } from '../../lib/utils' - -export interface PrePostTabProps extends HTMLAttributes { - preTab?: ReactNode | string - postTab?: ReactNode | string -} - -export const PrePostTab = forwardRef>( - ({ preTab, postTab, children, className, ...props }, ref) => { - return ( -
- {preTab && ( -
- {preTab} -
- )} -
- {children} -
- {postTab && ( -
- {postTab} -
- )} -
- ) - } -) diff --git a/packages/ui/src/components/PrePostTab/index.tsx b/packages/ui/src/components/PrePostTab/index.tsx deleted file mode 100644 index 89a9935b84a19..0000000000000 --- a/packages/ui/src/components/PrePostTab/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { PrePostTab } from './PrePostTab' -export type { PrePostTabProps } from './PrePostTab' diff --git a/packages/ui/src/components/shadcn/ui/input-group.tsx b/packages/ui/src/components/shadcn/ui/input-group.tsx new file mode 100644 index 0000000000000..343ad96e2951c --- /dev/null +++ b/packages/ui/src/components/shadcn/ui/input-group.tsx @@ -0,0 +1,159 @@ +import { cva, type VariantProps } from 'class-variance-authority' +import * as React from 'react' + +import { cn } from '../../../lib/utils/cn' +import { Button } from '../../Button' +import { Input, type InputProps } from './input' +import { Textarea, type TextareaProps } from './textarea' + +function InputGroup({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
textarea]:h-auto', + + // Variants based on alignment. + 'has-[>[data-align=inline-start]]:[&>input]:pl-2', + 'has-[>[data-align=inline-end]]:[&>input]:pr-2', + 'has-[>[data-align=block-end]]:pb-0', + 'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3', + 'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3', + + // Focus state. + 'has-[[data-slot=input-group-control]:focus-visible]:outline-none has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-background-control has-[[data-slot=input-group-control]:focus-visible]:ring-offset-2 has-[[data-slot=input-group-control]:focus-visible]:ring-offset-foreground-muted', + + // Error state. + 'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40', + + // Disabled state. + 'has-[[data-slot=input-group-control]:disabled]:cursor-not-allowed has-[[data-slot=input-group-control]:disabled]:text-foreground-muted', + + // Readonly state. + 'has-[[data-slot=input-group-control]:read-only]:border-button', + className + )} + {...props} + /> + ) +} + +const inputGroupAddonVariants = cva( + "text-foreground-light flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm 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-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]', + 'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]', + 'block-start': + '[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5', + 'block-end': + '[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5', + }, + }, + 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('', { + variants: { + size: { + tiny: "h-6 gap-1 rounded-md px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5", + small: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5', + }, + }, + defaultVariants: { + size: 'tiny', + }, +}) + +function InputGroupButton({ + className, + type = 'text', + size = 'tiny', + ...props +}: Omit, 'size'> & + VariantProps) { + return ( +