Spaces:
Build error
Build error
'use client'; | |
import { hiddenInputStyles } from '@/components/ui/input'; | |
import { Popover } from '@/components/ui/popover'; | |
import * as SwitchPrimitive from '@radix-ui/react-switch'; | |
import { cva, VariantProps } from 'class-variance-authority'; | |
import * as React from 'react'; | |
import { | |
AiOutlineExclamationCircle, | |
AiOutlineQuestionCircle, | |
} from 'react-icons/ai'; | |
import { | |
BasicField, | |
BasicFieldOptions, | |
extractBasicFieldProps, | |
} from '../basic-field'; | |
import { cn, ComponentAnatomy, defineStyleAnatomy } from '../core/styling'; | |
import { mergeRefs } from '../core/utils'; | |
/* ------------------------------------------------------------------------------------------------- | |
* Anatomy | |
* -----------------------------------------------------------------------------------------------*/ | |
export const SwitchAnatomy = defineStyleAnatomy({ | |
root: cva( | |
[ | |
'UI-Switch__root', | |
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors', | |
'disabled:cursor-not-allowed data-[disabled=true]:opacity-50', | |
'outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[--ring] focus-visible:ring-offset-1', | |
'data-[state=unchecked]:bg-gray-200 dark:data-[state=unchecked]:bg-gray-700', // Unchecked | |
'data-[state=unchecked]:hover:bg-gray-300 dark:data-[state=unchecked]:hover:bg-gray-600', // Unchecked hover | |
'data-[state=checked]:bg-brand', // Checked | |
'data-[error=true]:border-red-500', // Checked | |
], | |
{ | |
variants: { | |
size: { | |
sm: 'h-5 w-9', | |
md: 'h-6 w-11', | |
lg: 'h-7 w-14', | |
}, | |
}, | |
defaultVariants: { | |
size: 'md', | |
}, | |
} | |
), | |
container: cva(['UI-Switch__container', 'inline-flex gap-2 items-center'], { | |
variants: { | |
side: { | |
left: '', | |
right: 'w-full flex-row-reverse', | |
}, | |
}, | |
defaultVariants: { | |
side: 'left', | |
}, | |
}), | |
thumb: cva( | |
[ | |
'UI-Switch__thumb', | |
'pointer-events-none block rounded-full bg-white shadow-lg ring-0 transition-transform', | |
'data-[state=unchecked]:translate-x-1', | |
], | |
{ | |
variants: { | |
size: { | |
sm: 'h-3 w-3 data-[state=checked]:translate-x-[1.1rem]', | |
md: 'h-4 w-4 data-[state=checked]:translate-x-[1.4rem]', | |
lg: 'h-5 w-5 data-[state=checked]:translate-x-[1.9rem]', | |
}, | |
}, | |
defaultVariants: { | |
size: 'md', | |
}, | |
} | |
), | |
label: cva([ | |
'UI-Switch__label', | |
'relative font-normal', | |
'data-[disabled=true]:text-gray-300 cursor-pointer user-select-none select-none', | |
]), | |
}); | |
/* ------------------------------------------------------------------------------------------------- | |
* Switch | |
* -----------------------------------------------------------------------------------------------*/ | |
export type SwitchProps = BasicFieldOptions & | |
ComponentAnatomy<typeof SwitchAnatomy> & | |
VariantProps<typeof SwitchAnatomy.root> & | |
VariantProps<typeof SwitchAnatomy.container> & | |
Omit< | |
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>, | |
| 'value' | |
| 'checked' | |
| 'disabled' | |
| 'required' | |
| 'defaultValue' | |
| 'defaultChecked' | |
| 'onCheckedChange' | |
> & { | |
/** | |
* Whether the switch is checked | |
*/ | |
value?: boolean; | |
/** | |
* Callback fired when the value changes | |
*/ | |
onValueChange?: (value: boolean) => void; | |
/** | |
* Default value when uncontrolled | |
*/ | |
defaultValue?: boolean; | |
/** | |
* Ref to the input element | |
*/ | |
inputRef?: React.Ref<HTMLInputElement>; | |
className?: string; | |
moreHelp?: React.ReactNode; | |
}; | |
export const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>( | |
(props, ref) => { | |
const [ | |
{ | |
size, | |
value: controlledValue, | |
className, | |
onValueChange, | |
labelClass, | |
containerClass, | |
thumbClass, | |
defaultValue, | |
inputRef, | |
side, | |
moreHelp, | |
...rest | |
}, | |
{ label, ...basicFieldProps }, | |
] = extractBasicFieldProps(props, React.useId()); | |
const isFirst = React.useRef(true); | |
const buttonRef = React.useRef<HTMLButtonElement>(null); | |
const [_value, _setValue] = React.useState<boolean | undefined>( | |
controlledValue ?? defaultValue ?? false | |
); | |
const handleOnValueChange = React.useCallback((value: boolean) => { | |
_setValue(value); | |
onValueChange?.(value); | |
}, []); | |
React.useEffect(() => { | |
if (!defaultValue || !isFirst.current) { | |
_setValue(controlledValue); | |
} | |
isFirst.current = false; | |
}, [controlledValue]); | |
return ( | |
<BasicField | |
{...basicFieldProps} | |
id={basicFieldProps.id} | |
fieldClass={cn( | |
'w-fit', | |
side === 'right' && | |
'w-full group/switch hover:bg-white/5 rounded-[--radius] p-2 w-[calc(100%_+_1rem)] -ml-2' | |
)} | |
fieldHelpTextClass={cn('')} | |
> | |
<div className={cn(SwitchAnatomy.container({ side }), containerClass)}> | |
<SwitchPrimitive.Root | |
ref={mergeRefs([buttonRef, ref])} | |
id={basicFieldProps.id} | |
className={cn(SwitchAnatomy.root({ size }), className)} | |
disabled={basicFieldProps.disabled || basicFieldProps.readonly} | |
data-disabled={basicFieldProps.disabled} | |
data-readonly={basicFieldProps.readonly} | |
data-error={!!basicFieldProps.error} | |
checked={_value} | |
onCheckedChange={handleOnValueChange} | |
defaultChecked={defaultValue} | |
{...rest} | |
> | |
<SwitchPrimitive.Thumb | |
className={cn(SwitchAnatomy.thumb({ size }), thumbClass)} | |
/> | |
</SwitchPrimitive.Root> | |
<div className="flex flex-1"></div> | |
{!!label && ( | |
<div className="flex items-center gap-1"> | |
<label | |
className={cn( | |
SwitchAnatomy.label(), | |
labelClass, | |
side === 'right' && | |
'font-semibold transition-transform __group-hover/switch:-translate-y-0.5' | |
)} | |
htmlFor={basicFieldProps.id} | |
data-disabled={basicFieldProps.disabled} | |
> | |
{label} | |
</label> | |
{moreHelp && ( | |
<Popover | |
className="text-sm" | |
trigger={ | |
<AiOutlineExclamationCircle className="transition-opacity opacity-45 hover:opacity-90" /> | |
} | |
> | |
{moreHelp} | |
</Popover> | |
)} | |
</div> | |
)} | |
<input | |
ref={inputRef} | |
type="checkbox" | |
name={basicFieldProps.name} | |
className={hiddenInputStyles} | |
value={_value ? 'on' : 'off'} | |
checked={basicFieldProps.required ? _value : true} | |
aria-hidden="true" | |
required={controlledValue === undefined && basicFieldProps.required} | |
tabIndex={-1} | |
onChange={() => {}} | |
onFocusCapture={() => buttonRef.current?.focus()} | |
/> | |
</div> | |
</BasicField> | |
); | |
} | |
); | |
Switch.displayName = 'Switch'; | |