// AlignUI Button v0.0.0 import * as React from 'react'; import { tv, type VariantProps } from '@/utils/tv'; import { recursiveCloneChildren } from '@/utils/recursive-clone-children'; import type { PolymorphicComponentProps } from '@/utils/polymorphic'; import { Slot } from '@radix-ui/react-slot'; const BUTTON_ROOT_NAME = 'ButtonRoot'; const BUTTON_ICON_NAME = 'ButtonIcon'; function addTextSlots(children: React.ReactNode) { return React.Children.map(children, (child) => { if (typeof child === 'string' || typeof child === 'number') { return {child}; } return child; }); } function hasTextSlot(children: React.ReactNode) { return React.Children.toArray(children).some((child) => { if (typeof child === 'string' || typeof child === 'number') { return true; } return ( React.isValidElement(child) && 'data-text' in (child.props as Record) ); }); } export const buttonVariants = tv({ slots: { root: [ // base 'group relative inline-flex items-center justify-center whitespace-nowrap outline-none', 'transition duration-200 ease-out', // focus 'focus:outline-none', // disabled 'disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:pointer-events-none disabled:ring-transparent', ], icon: [ // base 'flex size-5 shrink-0 items-center justify-center', ], }, variants: { variant: { primary: {}, neutral: {}, error: {}, }, mode: { filled: {}, stroke: { root: 'ring-1 ring-inset', }, lighter: { root: 'ring-1 ring-inset', }, ghost: { root: 'ring-1 ring-inset', }, }, size: { medium: { root: 'rounded-10 text-label-sm h-10 gap-3 px-3.5', icon: '-mx-1', }, small: { root: 'text-label-sm h-9 gap-3 rounded-lg px-3', icon: '-mx-1', }, xsmall: { root: 'text-label-sm h-8 gap-2.5 rounded-lg px-2.5', icon: '-mx-1', }, xxsmall: { root: 'text-label-sm h-7 gap-2.5 rounded-lg px-2', icon: '-mx-1', }, }, }, compoundVariants: [ //#region variant=primary { variant: 'primary', mode: 'filled', class: { root: [ // base 'bg-primary-base text-static-white', // hover 'hover:bg-primary-darker', // focus 'focus-visible:shadow-button-primary-focus', ], }, }, { variant: 'primary', mode: 'stroke', class: { root: [ // base 'bg-bg-white-0 text-primary-base ring-primary-base', // hover 'hover:bg-primary-alpha/10 hover:ring-transparent', // focus 'focus-visible:shadow-button-primary-focus', ], }, }, { variant: 'primary', mode: 'lighter', class: { root: [ // base 'bg-primary-alpha/10 text-primary-base ring-transparent', // hover 'hover:bg-bg-white-0 hover:ring-primary-base', // focus 'focus-visible:bg-bg-white-0 focus-visible:shadow-button-primary-focus focus-visible:ring-primary-base', ], }, }, { variant: 'primary', mode: 'ghost', class: { root: [ // base 'text-primary-base bg-transparent ring-transparent', // hover 'hover:bg-primary-alpha/10', // focus 'focus-visible:bg-bg-white-0 focus-visible:shadow-button-primary-focus focus-visible:ring-primary-base', ], }, }, //#endregion //#region variant=neutral { variant: 'neutral', mode: 'filled', class: { root: [ // base 'bg-bg-strong-950 text-text-white-0', // hover 'hover:bg-bg-surface-800', // focus 'focus-visible:shadow-button-important-focus', ], }, }, { variant: 'neutral', mode: 'stroke', class: { root: [ // base 'bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200', // hover 'hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent', // focus 'focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950', ], }, }, { variant: 'neutral', mode: 'lighter', class: { root: [ // base 'bg-bg-weak-50 text-text-sub-600 ring-transparent', // hover 'hover:bg-bg-white-0 hover:text-text-strong-950 hover:shadow-regular-xs hover:ring-stroke-soft-200', // focus 'focus-visible:bg-bg-white-0 focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950', ], }, }, { variant: 'neutral', mode: 'ghost', class: { root: [ // base 'text-text-sub-600 bg-transparent ring-transparent', // hover 'hover:bg-bg-weak-50 hover:text-text-strong-950', // focus 'focus-visible:bg-bg-white-0 focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950', ], }, }, //#endregion //#region variant=error { variant: 'error', mode: 'filled', class: { root: [ // base 'bg-error-base text-static-white', // hover 'hover:bg-red-700', // focus 'focus-visible:shadow-button-error-focus', ], }, }, { variant: 'error', mode: 'stroke', class: { root: [ // base 'bg-bg-white-0 text-error-base ring-error-base', // hover 'hover:bg-red-alpha/10 hover:ring-transparent', // focus 'focus-visible:shadow-button-error-focus', ], }, }, { variant: 'error', mode: 'lighter', class: { root: [ // base 'bg-red-alpha/10 text-error-base ring-transparent', // hover 'hover:bg-bg-white-0 hover:ring-error-base', // focus 'focus-visible:bg-bg-white-0 focus-visible:shadow-button-error-focus focus-visible:ring-error-base', ], }, }, { variant: 'error', mode: 'ghost', class: { root: [ // base 'text-error-base bg-transparent ring-transparent', // hover 'hover:bg-red-alpha/10', // focus 'focus-visible:bg-bg-white-0 focus-visible:shadow-button-error-focus focus-visible:ring-error-base', ], }, }, //#endregion ], defaultVariants: { variant: 'primary', mode: 'filled', size: 'medium', }, }); type ButtonSharedProps = VariantProps; type ButtonRootProps = VariantProps & React.ButtonHTMLAttributes & { asChild?: boolean; }; /** * Unified action API for buttons and links. * * Existing `Button.Root` remains available for backwards compatibility. New * code can use `Button` for either a native button or an anchor and can rely * on the same disabled/loading props for both elements. */ export type ButtonProps = ButtonSharedProps & Omit, 'disabled'> & { href?: string; isDisabled?: boolean; isLoading?: boolean; showTextWhileLoading?: boolean; asChild?: boolean; }; const ButtonRoot = React.forwardRef( ( { children, variant, mode, size, asChild, className, ...rest }, forwardedRef, ) => { const uniqueId = React.useId(); const Component = asChild ? Slot : 'button'; const { root } = buttonVariants({ variant, mode, size }); const sharedProps: ButtonSharedProps = { variant, mode, size, }; const extendedChildren = recursiveCloneChildren( children as React.ReactElement[], sharedProps, [BUTTON_ICON_NAME], uniqueId, asChild, ); const slottedChildren = addTextSlots(extendedChildren); const iconOnly = !hasTextSlot(slottedChildren); return ( {slottedChildren} ); }, ); ButtonRoot.displayName = BUTTON_ROOT_NAME; const Button = React.forwardRef( ( { children, variant, mode, size, href, isDisabled = false, isLoading = false, showTextWhileLoading = false, asChild, className, ...rest }, forwardedRef, ) => { const uniqueId = React.useId(); const Component: React.ElementType = asChild ? Slot : href ? 'a' : 'button'; const isInactive = isDisabled || isLoading; const { root } = buttonVariants({ variant, mode, size }); const sharedProps: ButtonSharedProps = { variant, mode, size, }; const extendedChildren = recursiveCloneChildren( children as React.ReactElement[], sharedProps, [BUTTON_ICON_NAME], uniqueId, asChild, ); const slottedChildren = addTextSlots(extendedChildren); const iconOnly = !hasTextSlot(slottedChildren); const content = isLoading ? ( <> {slottedChildren} ) : ( slottedChildren ); return ( {content} ); }, ); Button.displayName = 'Button'; function ButtonIcon({ variant, mode, size, as, className, ...rest }: PolymorphicComponentProps) { const Component = as || 'div'; const { icon } = buttonVariants({ mode, variant, size }); return ( ); } ButtonIcon.displayName = BUTTON_ICON_NAME; function ButtonText({ className, children, ...rest }: React.HTMLAttributes) { return ( {children} ); } ButtonText.displayName = 'ButtonText'; export { Button, ButtonRoot as Root, ButtonIcon as Icon, ButtonText as Text };