@liguelead/design-system 0.0.10 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/components/Button/Button.appearance.ts +20 -16
  2. package/components/Button/Button.sizes.ts +18 -17
  3. package/components/Button/Button.styles.ts +7 -11
  4. package/components/Button/Button.tsx +5 -5
  5. package/components/Button/Button.types.ts +3 -3
  6. package/components/Checkbox/Checkbox.styles.ts +84 -35
  7. package/components/Checkbox/Checkbox.tsx +62 -5
  8. package/components/Checkbox/Checkbox.types.ts +1 -0
  9. package/components/IconButton/IconButton.sizes.ts +6 -13
  10. package/components/IconButton/IconButton.tsx +5 -5
  11. package/components/InputOpt/InputOpt.styles.ts +75 -0
  12. package/components/InputOpt/InputOpt.tsx +153 -0
  13. package/components/InputOpt/InputOpt.types.ts +14 -0
  14. package/components/InputOpt/index.ts +1 -0
  15. package/components/InputOpt/utils/focusManagement.ts +31 -0
  16. package/components/InputOpt/utils/index.ts +2 -0
  17. package/components/InputOpt/utils/inputValidation.ts +14 -0
  18. package/components/LinkButton/LinkButton.size.ts +39 -0
  19. package/components/LinkButton/LinkButton.style.ts +45 -0
  20. package/components/LinkButton/LinkButton.tsx +75 -0
  21. package/components/LinkButton/LinkButton.types.ts +11 -0
  22. package/components/LinkButton/index.ts +1 -0
  23. package/components/RadioButton/RadioButton.inputVariants.ts +133 -0
  24. package/components/RadioButton/RadioButton.styles.ts +78 -0
  25. package/components/RadioButton/RadioButton.tsx +88 -0
  26. package/components/RadioButton/RadioButton.types.ts +33 -0
  27. package/components/RadioButton/RadioButton.variants.ts +67 -0
  28. package/components/RadioButton/index.ts +1 -0
  29. package/components/SegmentedButton/SegmentedButton.tsx +0 -6
  30. package/components/Select/Select.sizes.ts +10 -11
  31. package/components/Select/Select.states.tsx +8 -37
  32. package/components/Select/Select.styles.ts +53 -39
  33. package/components/Select/Select.tsx +30 -23
  34. package/components/Select/Select.types.ts +1 -2
  35. package/components/Text/Text.styles.ts +11 -8
  36. package/components/Text/Text.tsx +2 -2
  37. package/components/Text/Text.types.ts +3 -1
  38. package/components/TextField/TextField.sizes.ts +3 -9
  39. package/components/TextField/TextField.states.tsx +11 -11
  40. package/components/TextField/TextField.styles.ts +4 -0
  41. package/components/TextField/TextField.tsx +5 -2
  42. package/components/index.ts +3 -0
  43. package/package.json +2 -2
  44. package/utils/darkenOrLighen.ts +1 -1
@@ -0,0 +1,153 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import { InputOptProps } from './InputOpt.types'
3
+ import {
4
+ OptWrapper,
5
+ OptBox,
6
+ OtpSeparator,
7
+ OptHelperText,
8
+ OtpSeparatorContainer,
9
+ } from './InputOpt.styles'
10
+
11
+ import {
12
+ isValidChar,
13
+ sanitizeValue,
14
+ createInitialState,
15
+ focusNext,
16
+ focusPrevious,
17
+ focusFirstEmpty
18
+ } from './utils'
19
+
20
+ const InputOpt: React.FC<InputOptProps> = ({
21
+ length = 6,
22
+ value,
23
+ onChange,
24
+ onComplete,
25
+ autoFocus = true,
26
+ disabled = false,
27
+ placeholderChar = '',
28
+ name = 'otp',
29
+ className,
30
+ inputMode = 'numeric',
31
+ helperText,
32
+ error,
33
+ ...rest
34
+ }) => {
35
+ const isControlled = value !== undefined
36
+ const [internal, setInternal] = useState<string[]>(() => createInitialState(value, length))
37
+ const refs = useRef<Array<HTMLInputElement | null>>([])
38
+
39
+ useEffect(() => {
40
+ if (!isControlled) return
41
+ const sanitized = sanitizeValue(value!, inputMode)
42
+ setInternal(createInitialState(sanitized, length))
43
+ }, [value, inputMode, isControlled, length])
44
+
45
+ useEffect(() => {
46
+ if (autoFocus && refs.current[0]) refs.current[0].focus()
47
+ }, [autoFocus])
48
+
49
+ const emitChange = (next: string[]) => {
50
+ const joined = next.join('')
51
+ onChange?.(joined)
52
+ if (joined.length === length && !next.includes('')) onComplete?.(joined)
53
+ }
54
+
55
+ const updateIndex = (index: number, char: string) => {
56
+ if (disabled || !isValidChar(char, inputMode)) return
57
+
58
+ const next = [...internal]
59
+ next[index] = char
60
+ if (!isControlled) setInternal(next)
61
+ emitChange(next)
62
+ focusNext(refs, index, length)
63
+ }
64
+
65
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => {
66
+ if (disabled) return
67
+ const { key } = e
68
+
69
+ if (key === 'Backspace') {
70
+ e.preventDefault()
71
+ const next = [...internal]
72
+ next[index] = ''
73
+ if (!isControlled) setInternal(next)
74
+ emitChange(next)
75
+ focusPrevious(refs, index)
76
+ return
77
+ }
78
+
79
+ if (key === 'ArrowLeft') focusPrevious(refs, index)
80
+ if (key === 'ArrowRight') focusNext(refs, index, length)
81
+ }
82
+
83
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
84
+ const char = e.target.value.slice(-1)
85
+ updateIndex(index, char)
86
+ }
87
+
88
+ const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>, index: number) => {
89
+ if (disabled) return
90
+ e.preventDefault()
91
+
92
+ const text = e.clipboardData.getData('text').trim()
93
+ if (!text) return
94
+
95
+ const valid = sanitizeValue(text, inputMode)
96
+ const next = [...internal]
97
+
98
+ for (let j = 0; j < valid.length && index + j < length; j++) {
99
+ next[index + j] = valid[j]
100
+ }
101
+
102
+ if (!isControlled) setInternal(next)
103
+ emitChange(next)
104
+ focusFirstEmpty(refs, next, length)
105
+ }
106
+
107
+ return (
108
+ <>
109
+ <OptWrapper
110
+ className={`${className || ''} ${error ? 'error' : ''}`.trim()}
111
+ aria-disabled={disabled}
112
+ aria-invalid={!!error}
113
+ role="group"
114
+ data-error={!!error}
115
+ >
116
+ {Array.from({ length }).map((_, i) => (
117
+ <React.Fragment key={i}>
118
+ <OptBox
119
+ ref={el => (refs.current[i] = el)}
120
+ $disabled={disabled}
121
+ $error={!!error}
122
+ value={internal[i] || ''}
123
+ placeholder={placeholderChar}
124
+ onKeyDown={e => handleKeyDown(e, i)}
125
+ onPaste={e => handlePaste(e, i)}
126
+ onChange={e => handleChange(e, i)}
127
+ {...rest}
128
+ inputMode={inputMode}
129
+ pattern={inputMode === 'numeric' ? '[0-9]*' : undefined}
130
+ autoComplete="one-time-code"
131
+ aria-label={`Dígito ${i + 1} de ${length}`}
132
+ disabled={disabled}
133
+ maxLength={1}
134
+ />
135
+ {(i + 1) % 2 === 0 && i < length - 1 && (
136
+ <OtpSeparatorContainer>
137
+ <OtpSeparator>-</OtpSeparator>
138
+ </OtpSeparatorContainer>
139
+ )}
140
+ </React.Fragment>
141
+ ))}
142
+ </OptWrapper>
143
+
144
+ {helperText && (
145
+ <OptHelperText $error={!!error} aria-live="polite">
146
+ {helperText}
147
+ </OptHelperText>
148
+ )}
149
+ </>
150
+ )
151
+ }
152
+
153
+ export default InputOpt
@@ -0,0 +1,14 @@
1
+ export interface InputOptProps {
2
+ length?: number;
3
+ value?: string;
4
+ onChange?: (code: string) => void;
5
+ onComplete?: (code: string) => void;
6
+ autoFocus?: boolean;
7
+ disabled?: boolean;
8
+ placeholderChar?: string;
9
+ name?: string;
10
+ className?: string;
11
+ inputMode?: 'numeric' | 'text';
12
+ helperText?: string;
13
+ error?: boolean;
14
+ }
@@ -0,0 +1 @@
1
+ export { default } from './InputOpt'
@@ -0,0 +1,31 @@
1
+ export const focusNext = (
2
+ refs: React.MutableRefObject<Array<HTMLInputElement | null>>,
3
+ currentIndex: number,
4
+ length: number
5
+ ) => {
6
+ if (currentIndex < length - 1) {
7
+ refs.current[currentIndex + 1]?.focus()
8
+ }
9
+ }
10
+
11
+ export const focusPrevious = (
12
+ refs: React.MutableRefObject<Array<HTMLInputElement | null>>,
13
+ currentIndex: number
14
+ ) => {
15
+ if (currentIndex > 0) {
16
+ refs.current[currentIndex - 1]?.focus()
17
+ }
18
+ }
19
+
20
+ export const focusFirstEmpty = (
21
+ refs: React.MutableRefObject<Array<HTMLInputElement | null>>,
22
+ values: string[],
23
+ length: number
24
+ ) => {
25
+ const firstEmpty = values.findIndex(c => c === '')
26
+ if (firstEmpty >= 0) {
27
+ refs.current[firstEmpty]?.focus()
28
+ } else {
29
+ refs.current[length - 1]?.blur()
30
+ }
31
+ }
@@ -0,0 +1,2 @@
1
+ export * from './inputValidation'
2
+ export * from './focusManagement'
@@ -0,0 +1,14 @@
1
+ export const isValidChar = (char: string, inputMode: 'numeric' | 'text'): boolean => {
2
+ return inputMode === 'numeric' ? /^\d$/.test(char) : /^[0-9a-zA-Z]$/.test(char)
3
+ }
4
+
5
+ export const sanitizeValue = (value: string, inputMode: 'numeric' | 'text'): string => {
6
+ return inputMode === 'numeric'
7
+ ? value.replace(/[^0-9]/g, '')
8
+ : value.replace(/[^0-9a-zA-Z]/g, '')
9
+ }
10
+
11
+ export const createInitialState = (value: string | undefined, length: number): string[] => {
12
+ const chars = (value || '').split('').slice(0, length)
13
+ return [...chars, ...Array(Math.max(0, length - chars.length)).fill('')]
14
+ }
@@ -0,0 +1,39 @@
1
+ import {
2
+ fontSize,
3
+ fontWeight,
4
+ lineHeight,
5
+ spacing,
6
+ radius
7
+ } from '@liguelead/foundation'
8
+ import { ButtonSizeTypes } from '../Button/Button.types'
9
+
10
+ export const LinkButtonSizes = (size: ButtonSizeTypes) => {
11
+ const sizes = {
12
+ sm: `
13
+ padding: ${spacing.spacing0}px ${spacing.spacing4}px;
14
+ font-size: ${fontSize.fontSize12}px;
15
+ font-weight: ${fontWeight.fontWeight500};
16
+ line-height: ${lineHeight.lineHeight22}px;
17
+ gap: ${spacing.spacing8}px;
18
+ border-radius: ${radius.radius4}px;
19
+ `,
20
+ md: `
21
+ padding: ${spacing.spacing0}px ${spacing.spacing4}px;
22
+ font-size: ${fontSize.fontSize14}px;
23
+ font-weight: ${fontWeight.fontWeight500};
24
+ line-height: ${lineHeight.lineHeight22}px;
25
+ gap: ${spacing.spacing8}px;
26
+ border-radius: ${radius.radius4}px;
27
+ `,
28
+ lg: `
29
+ padding: ${spacing.spacing0}px ${spacing.spacing4}px;
30
+ font-size: ${fontSize.fontSize16}px;
31
+ font-weight: ${fontWeight.fontWeight500};
32
+ line-height: ${lineHeight.lineHeight24}px;
33
+ gap: ${spacing.spacing8}px;
34
+ border-radius: ${radius.radius4}px;
35
+ `
36
+ }
37
+
38
+ return sizes[size]
39
+ }
@@ -0,0 +1,45 @@
1
+ import styled from 'styled-components'
2
+ import { parseColor } from '../../utils'
3
+ import { StyledButtonProps } from '../Button/Button.styles'
4
+ import { shadow } from '@liguelead/foundation'
5
+
6
+ export const LinkAnchor = styled.a`
7
+ text-decoration: none;
8
+ display: inline-flex;
9
+ align-items: center;
10
+
11
+ color: ${({ theme }) => parseColor(theme.colors.primary)};
12
+
13
+ &:hover {
14
+ text-decoration: underline;
15
+ background-color: transparent;
16
+ color: ${({ theme }) => parseColor(theme.colors.primaryDark)};
17
+ }
18
+
19
+ &:focus {
20
+ outline: 2px solid ${({ theme }) => parseColor(theme.colors.primaryLight)};
21
+ outline-offset: 2px;
22
+ }
23
+
24
+ &[aria-disabled='true'] {
25
+ opacity: .5;
26
+ pointer-events: none;
27
+ }
28
+ `
29
+
30
+ export const StyledLinkButton = styled.button<StyledButtonProps>`
31
+ position: relative;
32
+ display: flex;
33
+ outline: none !important;
34
+
35
+ width: ${({ $fluid }) => ($fluid ? '100%' : 'auto')};
36
+ ${({ $buttonSize }) => $buttonSize}
37
+ overflow: hidden;
38
+ cursor: pointer;
39
+ outline: none;
40
+ transition: background-color 0.3s, box-shadow 0.3s;
41
+
42
+ &:focus {
43
+ box-shadow: ${shadow.focusShadow};
44
+ }
45
+ `
@@ -0,0 +1,75 @@
1
+ import React, { useState } from 'react'
2
+ import { LinkAnchor, StyledLinkButton } from './LinkButton.style'
3
+ import { RippleContainer } from '../Button/Button.styles'
4
+ import { RippleInterface } from '../Button/Button.types'
5
+ import { ButtonVariant } from '../Button/Button.appearance'
6
+ import { LinkButtonSizes } from './LinkButton.size'
7
+ import { LinkButtonProps } from './LinkButton.types'
8
+
9
+ const LinkButton: React.FC<LinkButtonProps> = ({
10
+ href,
11
+ children,
12
+ disabled = false,
13
+ color = 'primary',
14
+ variant = 'ghost',
15
+ fluid = false,
16
+ leftIcon,
17
+ rightIcon,
18
+ onClick,
19
+ size = 'md',
20
+ ...rest
21
+ }) => {
22
+ const [ripples, setRipples] = useState<RippleInterface[]>([])
23
+ const buttonVariant = ButtonVariant(color, variant)
24
+ const buttonSize = LinkButtonSizes(size)
25
+
26
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
27
+ if (disabled) {
28
+ e.preventDefault()
29
+ return
30
+ }
31
+ const rect = e.currentTarget.getBoundingClientRect()
32
+ const x = e.clientX - rect.left
33
+ const y = e.clientY - rect.top
34
+ const newRipple: RippleInterface = {
35
+ id: `${Date.now()}-${Math.random()}`,
36
+ x,
37
+ y
38
+ }
39
+ setRipples(prev => [...prev, newRipple])
40
+ onClick?.(e)
41
+ }
42
+
43
+ return (
44
+ <StyledLinkButton
45
+ as={LinkAnchor}
46
+ href={href}
47
+ onClick={handleClick}
48
+ $variant={buttonVariant}
49
+ $buttonSize={buttonSize}
50
+ $fluid={fluid}
51
+ aria-disabled={disabled}
52
+ rel={rest.target === '_blank' ? 'noopener noreferrer' : rest.rel}
53
+ {...rest}
54
+ >
55
+ {leftIcon && <span className="link-btn__icon--left">{leftIcon}</span>}
56
+ <span className="link-btn__content">{children}</span>
57
+ {rightIcon && <span className="link-btn__icon--right">{rightIcon}</span>}
58
+ {ripples.map(r => (
59
+ <RippleContainer
60
+ key={r.id}
61
+ $variant={variant}
62
+ style={{
63
+ top: r.y - 10,
64
+ left: r.x - 10,
65
+ width: 20,
66
+ height: 20
67
+ }}
68
+ onAnimationEnd={() => setRipples(prev => prev.filter(x => x.id !== r.id))}
69
+ />
70
+ ))}
71
+ </StyledLinkButton>
72
+ )
73
+ }
74
+
75
+ export default LinkButton
@@ -0,0 +1,11 @@
1
+ export interface LinkButtonProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
2
+ href: string
3
+ children: React.ReactNode
4
+ disabled?: boolean
5
+ color?: string
6
+ variant?: 'ghost'
7
+ fluid?: boolean
8
+ leftIcon?: React.ReactNode
9
+ rightIcon?: React.ReactNode
10
+ size?: 'sm' | 'md' | 'lg'
11
+ }
@@ -0,0 +1 @@
1
+ export { default } from './LinkButton';
@@ -0,0 +1,133 @@
1
+ import { css } from 'styled-components'
2
+ import { spacing, shadow } from '@liguelead/foundation'
3
+ import { parseColor } from '../../utils'
4
+
5
+ interface RadioInputVariantProps {
6
+ $error?: boolean
7
+ $variant?: 'default' | 'box'
8
+ }
9
+
10
+ export const RadioInputVariant = (variant: 'default' | 'box') => {
11
+ const variants = {
12
+ default: css<RadioInputVariantProps>`
13
+ appearance: none;
14
+ width: ${spacing.spacing16}px;
15
+ height: ${spacing.spacing16}px;
16
+ border: 1px solid ${({ theme, $error }) =>
17
+ $error ? parseColor(theme.colors.danger200) : parseColor(theme.colors.neutral400)};
18
+ border-radius: 50%;
19
+ background: ${({ theme }) => parseColor(theme.colors.white)};
20
+ position: relative;
21
+ cursor: pointer;
22
+ transition: all 0.2s ease;
23
+ flex-shrink: 0;
24
+ margin-top: 2px;
25
+
26
+ &:checked {
27
+ border-color: ${({ theme, $error }) =>
28
+ $error ? parseColor(theme.colors.danger200) : parseColor(theme.colors.neutral300)};
29
+
30
+ &::after {
31
+ content: '';
32
+ position: absolute;
33
+ top: 50%;
34
+ left: 50%;
35
+ transform: translate(-50%, -50%);
36
+ width: 8px;
37
+ height: 8px;
38
+ border-radius: 50%;
39
+ background: ${({ theme, $error }) =>
40
+ $error ? parseColor(theme.colors.danger200) : parseColor(theme.colors.primary)};
41
+ }
42
+ }
43
+
44
+ &:focus {
45
+ outline: none;
46
+ box-shadow: ${({ theme, $error }) =>
47
+ $error
48
+ ? `0 0 0 3px ${parseColor(theme.colors.danger100)}40`
49
+ : `${shadow.focusShadow}`
50
+ };
51
+ border-color: ${({ theme, $error }) =>
52
+ $error
53
+ ? parseColor(theme.colors.danger200)
54
+ : parseColor(theme.colors.neutral700)
55
+ };
56
+ }
57
+
58
+ &:disabled {
59
+ cursor: not-allowed;
60
+ border-color: ${({ theme }) => parseColor(theme.colors.neutral400)};
61
+ background: ${({ theme }) => parseColor(theme.colors.neutral100)};
62
+
63
+ &:checked::after {
64
+ background: ${({ theme }) => parseColor(theme.colors.neutral400)};
65
+ }
66
+ }
67
+ `,
68
+
69
+ box: css<RadioInputVariantProps>`
70
+ appearance: none;
71
+ width: ${spacing.spacing16}px;
72
+ height: ${spacing.spacing16}px;
73
+ border: 1px solid ${({ theme, $error }) =>
74
+ $error ? parseColor(theme.colors.danger200) : parseColor(theme.colors.neutral400)};
75
+ border-radius: 50%;
76
+ background: ${({ theme }) => parseColor(theme.colors.white)};
77
+ position: relative;
78
+ cursor: pointer;
79
+ transition: all 0.2s ease;
80
+ flex-shrink: 0;
81
+ margin-top: 2px;
82
+
83
+ &:checked {
84
+ border-color: ${({ theme, $error }) =>
85
+ $error ? parseColor(theme.colors.danger200) : 'transparent'};
86
+ background: ${({ theme, $error }) =>
87
+ $error ? parseColor(theme.colors.danger200) : parseColor(theme.colors.primary)};
88
+
89
+ &::after {
90
+ content: '';
91
+ position: absolute;
92
+ top: 50%;
93
+ left: 50%;
94
+ transform: translate(-50%, -50%);
95
+ width: 8px;
96
+ height: 8px;
97
+ border-radius: 50%;
98
+ background: ${({ theme }) => parseColor(theme.colors.white)};
99
+ }
100
+ }
101
+
102
+ &:focus {
103
+ outline: none;
104
+ box-shadow: ${({ theme, $error }) =>
105
+ $error
106
+ ? `0 0 0 3px ${parseColor(theme.colors.danger100)}40`
107
+ : `${shadow.focusShadow}`
108
+ };
109
+ border-color: ${({ theme, $error }) =>
110
+ $error
111
+ ? parseColor(theme.colors.danger200)
112
+ : parseColor(theme.colors.neutral700)
113
+ };
114
+ }
115
+
116
+ &:disabled {
117
+ cursor: not-allowed;
118
+ border-color: ${({ theme }) => parseColor(theme.colors.neutral400)};
119
+ background: ${({ theme }) => parseColor(theme.colors.neutral100)};
120
+
121
+ &:checked {
122
+ background: ${({ theme }) => parseColor(theme.colors.neutral400)};
123
+
124
+ &::after {
125
+ background: ${({ theme }) => parseColor(theme.colors.white)};
126
+ }
127
+ }
128
+ }
129
+ `
130
+ }
131
+
132
+ return variants[variant] || variants.default
133
+ }
@@ -0,0 +1,78 @@
1
+ import styled from 'styled-components'
2
+ import {
3
+ fontSize,
4
+ fontWeight,
5
+ lineHeight,
6
+ spacing
7
+ } from '@liguelead/foundation'
8
+ import { parseColor } from '../../utils'
9
+ import { RadioButtonVariant } from './RadioButton.variants'
10
+ import { RadioInputVariant } from './RadioButton.inputVariants'
11
+ import { RadioWrapperProps } from './RadioButton.types'
12
+
13
+ export const RadioWrapper = styled.label<RadioWrapperProps>`
14
+ display: flex;
15
+ align-items: flex-start;
16
+ gap: ${spacing.spacing12}px;
17
+ cursor: ${({ $disabled }) => $disabled ? 'not-allowed' : 'pointer'};
18
+ opacity: ${({ $disabled }) => $disabled ? 0.5 : 1};
19
+ transition: all 0.2s ease;
20
+
21
+ ${({ $variant = 'default' }) => RadioButtonVariant($variant)}
22
+ `
23
+
24
+ interface RadioInputProps {
25
+ $error?: boolean
26
+ $variant?: 'default' | 'box'
27
+ }
28
+
29
+ export const RadioInput = styled.input<RadioInputProps>`
30
+ ${({ $variant = 'default' }) => RadioInputVariant($variant)}
31
+ `
32
+
33
+ export const RadioContent = styled.div`
34
+ display: flex;
35
+ flex-direction: column;
36
+ gap: ${spacing.spacing4}px;
37
+ flex: 1;
38
+ `
39
+
40
+ export const RadioContentWithIcon = styled.div`
41
+ display: flex;
42
+ align-items: flex-start;
43
+ justify-content: space-between;
44
+ width: 100%;
45
+ `
46
+
47
+ export const RadioIconWrapper = styled.div<{ $error?: boolean }>`
48
+ display: flex;
49
+ align-items: center;
50
+ margin-left: ${spacing.spacing8}px;
51
+ flex-shrink: 0;
52
+
53
+ & svg {
54
+ width: 20px;
55
+ height: 20px;
56
+ color: ${({ theme, $error }) =>
57
+ $error ? parseColor(theme.colors.danger100) : parseColor(theme.colors.neutral700)};
58
+ }
59
+ `
60
+
61
+ export const RadioLabel = styled.span<{ $error?: boolean }>`
62
+ font-size: ${fontSize.fontSize14}px;
63
+ font-weight: ${fontWeight.fontWeight500};
64
+ color: ${({ theme, $error }) =>
65
+ $error
66
+ ? parseColor(theme.colors.danger100)
67
+ : parseColor(theme.colors.textDark)};
68
+ `
69
+
70
+ export const RadioDescription = styled.span<{ $error?: boolean }>`
71
+ font-size: ${fontSize.fontSize12}px;
72
+ font-weight: ${fontWeight.fontWeight400};
73
+ line-height: ${lineHeight.lineHeight16}px;
74
+ color: ${({ theme, $error }) =>
75
+ $error
76
+ ? parseColor(theme.colors.danger100)
77
+ : parseColor(theme.colors.textMedium)};
78
+ `
@@ -0,0 +1,88 @@
1
+ import { forwardRef } from 'react'
2
+ import { RadioButtonProps } from './RadioButton.types'
3
+ import {
4
+ RadioWrapper,
5
+ RadioInput,
6
+ RadioContent,
7
+ RadioContentWithIcon,
8
+ RadioLabel,
9
+ RadioDescription,
10
+ RadioIconWrapper
11
+ } from './RadioButton.styles'
12
+
13
+ const RadioButton = forwardRef<HTMLInputElement, RadioButtonProps>(
14
+ (
15
+ {
16
+ className,
17
+ disabled = false,
18
+ error,
19
+ label,
20
+ description,
21
+ value,
22
+ checked,
23
+ onChange,
24
+ name,
25
+ register,
26
+ rightIcon,
27
+ variant = 'default',
28
+ ...props
29
+ },
30
+ ref
31
+ ) => {
32
+ const hasError = !!error
33
+
34
+ const contentElement = (
35
+ <RadioContent>
36
+ <RadioLabel $error={hasError}>
37
+ {label}
38
+ </RadioLabel>
39
+ {description && (
40
+ <RadioDescription $error={hasError}>
41
+ {description}
42
+ </RadioDescription>
43
+ )}
44
+ </RadioContent>
45
+ )
46
+
47
+ return (
48
+ <RadioWrapper
49
+ className={className}
50
+ $disabled={disabled}
51
+ $error={hasError}
52
+ $variant={variant}
53
+ $checked={checked}
54
+ >
55
+ <RadioInput
56
+ ref={ref}
57
+ type="radio"
58
+ value={value}
59
+ $variant={variant}
60
+ checked={checked}
61
+ disabled={disabled}
62
+ name={name}
63
+ $error={hasError}
64
+ {...props}
65
+ {...register}
66
+ onChange={(e) => {
67
+ register?.onChange(e)
68
+ onChange?.(e)
69
+ }}
70
+ />
71
+ {rightIcon ? (
72
+ <RadioContentWithIcon>
73
+ {contentElement}
74
+ <RadioIconWrapper $error={hasError}>
75
+ {rightIcon}
76
+ </RadioIconWrapper>
77
+ </RadioContentWithIcon>
78
+ ) : (
79
+ contentElement
80
+ )}
81
+ </RadioWrapper>
82
+ )
83
+ }
84
+ )
85
+
86
+ RadioButton.displayName = 'RadioButton'
87
+
88
+ export default RadioButton