@liguelead/design-system 0.0.9 → 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 (76) hide show
  1. package/components/Button/Button.appearance.ts +66 -62
  2. package/components/Button/Button.sizes.ts +42 -41
  3. package/components/Button/Button.styles.ts +46 -50
  4. package/components/Button/Button.tsx +76 -76
  5. package/components/Button/Button.types.ts +23 -23
  6. package/components/Button/index.ts +1 -1
  7. package/components/Checkbox/Checkbox.styles.ts +115 -66
  8. package/components/Checkbox/Checkbox.tsx +97 -40
  9. package/components/Checkbox/Checkbox.types.ts +12 -11
  10. package/components/Checkbox/index.tsx +2 -2
  11. package/components/DatePicker/DatePicker.styles.ts +66 -66
  12. package/components/DatePicker/DatePicker.tsx +135 -135
  13. package/components/DatePicker/DatePicker.types.ts +29 -29
  14. package/components/DatePicker/index.ts +1 -1
  15. package/components/IconButton/IconButton.sizes.ts +34 -41
  16. package/components/IconButton/IconButton.tsx +70 -70
  17. package/components/IconButton/index.ts +1 -1
  18. package/components/InputOpt/InputOpt.styles.ts +75 -0
  19. package/components/InputOpt/InputOpt.tsx +153 -0
  20. package/components/InputOpt/InputOpt.types.ts +14 -0
  21. package/components/InputOpt/index.ts +1 -0
  22. package/components/InputOpt/utils/focusManagement.ts +31 -0
  23. package/components/InputOpt/utils/index.ts +2 -0
  24. package/components/InputOpt/utils/inputValidation.ts +14 -0
  25. package/components/LinkButton/LinkButton.size.ts +39 -0
  26. package/components/LinkButton/LinkButton.style.ts +45 -0
  27. package/components/LinkButton/LinkButton.tsx +75 -0
  28. package/components/LinkButton/LinkButton.types.ts +11 -0
  29. package/components/LinkButton/index.ts +1 -0
  30. package/components/PageWrapper/PageWrapper.tsx +31 -31
  31. package/components/PageWrapper/index.ts +1 -1
  32. package/components/RadioButton/RadioButton.inputVariants.ts +133 -0
  33. package/components/RadioButton/RadioButton.styles.ts +78 -0
  34. package/components/RadioButton/RadioButton.tsx +88 -0
  35. package/components/RadioButton/RadioButton.types.ts +33 -0
  36. package/components/RadioButton/RadioButton.variants.ts +67 -0
  37. package/components/RadioButton/index.ts +1 -0
  38. package/components/SegmentedButton/SegmentedButton.styles.ts +29 -29
  39. package/components/SegmentedButton/SegmentedButton.tsx +43 -49
  40. package/components/SegmentedButton/SegmentedButton.types.ts +20 -20
  41. package/components/SegmentedButton/index.ts +1 -1
  42. package/components/Select/Select.sizes.ts +55 -56
  43. package/components/Select/Select.states.tsx +40 -69
  44. package/components/Select/Select.styles.ts +162 -148
  45. package/components/Select/Select.tsx +152 -144
  46. package/components/Select/Select.types.ts +35 -36
  47. package/components/Select/index.ts +1 -1
  48. package/components/Text/Text.styles.ts +46 -43
  49. package/components/Text/Text.tsx +27 -27
  50. package/components/Text/Text.types.ts +44 -42
  51. package/components/Text/index.ts +1 -1
  52. package/components/TextField/TextField.sizes.ts +52 -58
  53. package/components/TextField/TextField.states.tsx +53 -76
  54. package/components/TextField/TextField.styles.ts +85 -98
  55. package/components/TextField/TextField.tsx +108 -108
  56. package/components/TextField/TextField.types.ts +22 -21
  57. package/components/TextField/index.ts +2 -2
  58. package/components/TextField/utils/getState.ts +7 -0
  59. package/components/ThemeProvider/ThemeProvider.tsx +21 -21
  60. package/components/ThemeProvider/index.ts +1 -1
  61. package/components/ThemeProvider/style.ts +969 -969
  62. package/components/Wizard/StepContent.tsx +28 -28
  63. package/components/Wizard/StepMenuItem.tsx +33 -33
  64. package/components/Wizard/Wizard.context.tsx +76 -76
  65. package/components/Wizard/Wizard.styles.ts +126 -126
  66. package/components/Wizard/Wizard.tsx +55 -55
  67. package/components/Wizard/index.ts +1 -1
  68. package/components/index.ts +14 -11
  69. package/package.json +41 -41
  70. package/scripts/createTypes.js +70 -70
  71. package/utils/colorDarken.ts +10 -10
  72. package/utils/colorLighten.ts +10 -10
  73. package/utils/darkenOrLighen.ts +19 -19
  74. package/utils/getTextColor.ts +12 -12
  75. package/utils/index.ts +5 -5
  76. package/utils/parseColor.ts +7 -7
@@ -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';
@@ -1,31 +1,31 @@
1
- import {parseColor} from '../../utils'
2
- import styled from 'styled-components'
3
- import {colorType} from 'types'
4
- import {spacing, SpacingInterface} from '@liguelead/foundation'
5
-
6
- type SpacingType = keyof SpacingInterface
7
-
8
- interface WrapperProps {
9
- width?: string
10
- bgColor?: colorType
11
- padding?: SpacingType
12
- }
13
-
14
- const PageWrapper = styled.div.withConfig({
15
- shouldForwardProp: prop => !['bgColor', 'width', 'padding'].includes(prop)
16
- })<WrapperProps>`
17
- width: 100%;
18
- height: 100%;
19
- max-width: ${({width}) => width + 'px' || '100%'};
20
- margin: 0 auto;
21
- padding: ${({padding}) =>
22
- padding ? spacing[padding] + 'px' : spacing.spacing16 + 'px'};
23
- border-radius: 4px;
24
- background: ${({bgColor, theme}) =>
25
- bgColor
26
- ? parseColor(theme.colors[bgColor])
27
- : parseColor(theme.colors.white)};
28
- border: 1px solid ${({theme}) => parseColor(theme.colors.neutral300)};
29
- `
30
-
31
- export default PageWrapper
1
+ import {parseColor} from '../../utils'
2
+ import styled from 'styled-components'
3
+ import {colorType} from 'types'
4
+ import {spacing, SpacingInterface} from '@liguelead/foundation'
5
+
6
+ type SpacingType = keyof SpacingInterface
7
+
8
+ interface WrapperProps {
9
+ width?: string
10
+ bgColor?: colorType
11
+ padding?: SpacingType
12
+ }
13
+
14
+ const PageWrapper = styled.div.withConfig({
15
+ shouldForwardProp: prop => !['bgColor', 'width', 'padding'].includes(prop)
16
+ })<WrapperProps>`
17
+ width: 100%;
18
+ height: 100%;
19
+ max-width: ${({width}) => width + 'px' || '100%'};
20
+ margin: 0 auto;
21
+ padding: ${({padding}) =>
22
+ padding ? spacing[padding] + 'px' : spacing.spacing16 + 'px'};
23
+ border-radius: 4px;
24
+ background: ${({bgColor, theme}) =>
25
+ bgColor
26
+ ? parseColor(theme.colors[bgColor])
27
+ : parseColor(theme.colors.white)};
28
+ border: 1px solid ${({theme}) => parseColor(theme.colors.neutral300)};
29
+ `
30
+
31
+ export default PageWrapper
@@ -1 +1 @@
1
- export { default } from './PageWrapper'
1
+ export { default } from './PageWrapper'
@@ -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
+ `