@liguelead/design-system 0.0.25 → 0.0.27

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.
@@ -53,8 +53,8 @@ export const ButtonVariant = (
53
53
  }
54
54
  &:disabled {
55
55
  background-color: transparent;
56
- color: ${parseColor(theme.colors.neutral1100)}80;
57
- border-color: ${parseColor(theme.colors.neutral400)};
56
+ color: ${parsedColor}80;
57
+ border-color: ${parsedColor}80;
58
58
  cursor: not-allowed;
59
59
  }
60
60
  `,
@@ -68,7 +68,7 @@ export const ButtonVariant = (
68
68
  }
69
69
  &:disabled {
70
70
  background-color: transparent;
71
- color: ${parseColor(theme.colors.neutral1100)}80;
71
+ color: ${parsedColor}80;
72
72
  cursor: not-allowed;
73
73
  }
74
74
  `,
@@ -15,7 +15,7 @@ export const ButtonSizes = (size: ButtonSizeTypes) => {
15
15
  font-weight: ${fontWeight.fontWeight500};
16
16
  line-height: ${lineHeight.lineHeight22}px;
17
17
  gap: ${spacing.spacing8}px;
18
- min-width: ${spacing.spacing64}px;
18
+ height: ${spacing.spacing32}px;
19
19
  border-radius: ${radius.radius4}px;
20
20
  `,
21
21
  md: `
@@ -24,7 +24,7 @@ export const ButtonSizes = (size: ButtonSizeTypes) => {
24
24
  font-weight: ${fontWeight.fontWeight500};
25
25
  line-height: ${lineHeight.lineHeight22}px;
26
26
  gap: ${spacing.spacing8}px;
27
- min-width: ${spacing.spacing76}px;
27
+ height: ${spacing.spacing36}px;
28
28
  border-radius: ${radius.radius4}px;
29
29
  `,
30
30
  lg: `
@@ -33,7 +33,7 @@ export const ButtonSizes = (size: ButtonSizeTypes) => {
33
33
  font-weight: ${fontWeight.fontWeight500};
34
34
  line-height: ${lineHeight.lineHeight24}px;
35
35
  gap: ${spacing.spacing8}px;
36
- min-width: ${spacing.spacing108}px;
36
+ height: ${spacing.spacing40}px;
37
37
  border-radius: ${radius.radius4}px;
38
38
  `
39
39
  }
@@ -5,7 +5,7 @@ import { shadow } from '@liguelead/foundation'
5
5
  export interface StyledButtonProps extends ButtonProps {
6
6
  $variant: string
7
7
  $buttonSize: string
8
- $fluid: boolean
8
+ $fullWidth: boolean
9
9
  }
10
10
 
11
11
  export const StyledButton = styled.button<StyledButtonProps>`
@@ -14,7 +14,7 @@ export const StyledButton = styled.button<StyledButtonProps>`
14
14
  outline: none !important;
15
15
  justify-content: center;
16
16
  align-items: center;
17
- width: ${({ $fluid }) => ($fluid ? '100%' : 'auto')};
17
+ width: ${({ $fullWidth }) => ($fullWidth ? '100%' : 'auto')};
18
18
  ${({ $buttonSize }) => $buttonSize}
19
19
  overflow: hidden;
20
20
  cursor: pointer;
@@ -10,7 +10,7 @@ const Button: React.FC<ButtonProps> = ({
10
10
  className,
11
11
  color = 'primary',
12
12
  disabled,
13
- fluid = false,
13
+ fullWidth = false,
14
14
  size = 'md',
15
15
  onClick,
16
16
  type = 'button',
@@ -47,7 +47,7 @@ const Button: React.FC<ButtonProps> = ({
47
47
  <StyledButton
48
48
  disabled={disabled}
49
49
  className={className}
50
- $fluid={fluid}
50
+ $fullWidth={fullWidth}
51
51
  $variant={buttonVariant}
52
52
  onClick={handleClick}
53
53
  $buttonSize={buttonSize}
@@ -8,7 +8,7 @@ export interface ButtonProps {
8
8
  children: React.ReactNode
9
9
  className?: string
10
10
  disabled?: boolean
11
- fluid?: boolean
11
+ fullWidth?: boolean
12
12
  size?: ButtonSizeTypes
13
13
  color?: colorType
14
14
  onClick?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
@@ -0,0 +1,269 @@
1
+ import styled from 'styled-components'
2
+ import * as Popover from '@radix-ui/react-popover'
3
+ import { Command } from 'cmdk'
4
+ import {
5
+ fontSize,
6
+ fontWeight,
7
+ lineHeight,
8
+ shadow,
9
+ spacing
10
+ } from '@liguelead/foundation'
11
+ import { parseColor } from '../../utils'
12
+ import { StyledInput } from '../TextField/TextField.styles'
13
+ import { TextFieldSizeType } from '../TextField/TextField.sizes'
14
+ import { StateInterface } from '../TextField/TextField.states'
15
+ import Text from '../Text'
16
+
17
+ type TStyledInputProps = {
18
+ size: TextFieldSizeType
19
+ $themefication: StateInterface
20
+ }
21
+
22
+ export const TriggerButton = styled(StyledInput).attrs({ as: 'button' })<{
23
+ $width: number | string
24
+ }>`
25
+ width: ${({ $width }) =>
26
+ typeof $width === 'number' ? `${$width}px` : $width};
27
+ border-radius: 4px;
28
+ padding-right: ${spacing.spacing16}px !important;
29
+ background: transparent;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: space-between;
33
+ gap: ${spacing.spacing8}px;
34
+ cursor: pointer;
35
+ text-align: left;
36
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
37
+
38
+ &:hover:not(:disabled) {
39
+ border-color: ${({ theme }) => parseColor(theme.colors.neutral700)};
40
+ }
41
+
42
+ &:focus-visible {
43
+ border-color: ${({ theme }) => parseColor(theme.colors.neutral700)};
44
+ box-shadow: ${shadow.focusShadow};
45
+ outline: none;
46
+ }
47
+
48
+ &:disabled {
49
+ cursor: not-allowed;
50
+ background: transparent;
51
+ }
52
+ `
53
+
54
+ export const Label = styled.label`
55
+ display: block;
56
+ font-weight: ${fontWeight.fontWeight500};
57
+ font-size: ${fontSize.fontSize14}px;
58
+ `
59
+
60
+ export const HelperText = styled.span<{ error?: boolean }>`
61
+ font-size: ${fontSize.fontSize12}px;
62
+ line-height: ${lineHeight.lineHeight16}px;
63
+ `
64
+
65
+ export const TriggerLabel = styled.span`
66
+ min-width: 0;
67
+ overflow: hidden;
68
+ text-overflow: ellipsis;
69
+ white-space: nowrap;
70
+ `
71
+
72
+ export const TriggerIcon = styled.span`
73
+ display: inline-flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ color: ${({ theme }) => parseColor(theme.colors.neutral700)};
77
+ `
78
+
79
+ export const PopoverContent = styled(Popover.Content)<{
80
+ $width: number | string
81
+ }>`
82
+ width: ${({ $width }) =>
83
+ typeof $width === 'number' ? `${$width}px` : $width};
84
+ background: ${({ theme }) => parseColor(theme.colors.white)};
85
+ border: 1px solid ${({ theme }) => parseColor(theme.colors.neutral400)};
86
+ border-radius: 4px;
87
+ box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.08);
88
+ z-index: 10;
89
+ `
90
+
91
+ export const CommandRoot = styled(Command)`
92
+ width: 100%;
93
+ `
94
+
95
+ export const SearchRow = styled.div`
96
+ display: flex;
97
+ align-items: center;
98
+ gap: ${spacing.spacing8}px;
99
+ padding: ${spacing.spacing8}px ${spacing.spacing12}px;
100
+ border-bottom: 1px solid ${({ theme }) => parseColor(theme.colors.neutral200)};
101
+ `
102
+
103
+ export const SearchIcon = styled.div`
104
+ width: 16px;
105
+ height: 16px;
106
+ display: inline-flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ color: ${({ theme }) => parseColor(theme.colors.textDark)};
110
+ `
111
+
112
+ export const CommandInput = styled(Command.Input)`
113
+ width: 100%;
114
+ border: 0;
115
+ outline: none;
116
+ font-size: ${fontSize.fontSize14}px;
117
+ line-height: ${lineHeight.lineHeight20}px;
118
+ color: ${({ theme }) => parseColor(theme.colors.textDark)};
119
+ background: transparent;
120
+
121
+ &::placeholder {
122
+ color: ${({ theme }) => parseColor(theme.colors.textMedium)};
123
+ }
124
+ `
125
+
126
+ export const List = styled(Command.List)`
127
+ max-height: 260px;
128
+ overflow: auto;
129
+ padding-top: ${spacing.spacing8}px;
130
+
131
+ &::-webkit-scrollbar {
132
+ width: 10px;
133
+ }
134
+
135
+ &::-webkit-scrollbar-thumb {
136
+ background: ${({ theme }) => parseColor(theme.colors.neutral200)};
137
+ border-radius: 999px;
138
+ border: 3px solid ${({ theme }) => parseColor(theme.colors.white)};
139
+ }
140
+ `
141
+
142
+ export const Empty = styled(Command.Empty)`
143
+ padding: ${spacing.spacing8}px ${spacing.spacing12}px;
144
+ `
145
+
146
+ export const GroupHeading = styled.div`
147
+ padding: ${spacing.spacing12}px ${spacing.spacing12}px ${spacing.spacing4}px;
148
+ font-size: ${fontSize.fontSize12}px;
149
+ font-weight: ${fontWeight.fontWeight500};
150
+ color: ${({ theme }) => parseColor(theme.colors.textMedium)};
151
+ `
152
+
153
+ export const GroupWrap = styled(Command.Group)`
154
+ background: transparent;
155
+ `
156
+
157
+ export const ItemRow = styled(Command.Item)`
158
+ height: ${spacing.spacing36}px;
159
+ padding: 0 ${spacing.spacing12}px;
160
+ margin-bottom: ${spacing.spacing4}px;
161
+ display: flex;
162
+ align-items: center;
163
+ gap: ${spacing.spacing8}px;
164
+ font-size: ${fontSize.fontSize14}px;
165
+ color: ${({ theme }) => parseColor(theme.colors.textDark)};
166
+ cursor: pointer;
167
+ user-select: none;
168
+ transition: background-color 0.2s ease;
169
+
170
+ &[data-disabled='true'] {
171
+ opacity: 0.6;
172
+ cursor: not-allowed;
173
+ }
174
+
175
+ &:last-child {
176
+ margin-bottom: 0;
177
+ }
178
+
179
+ &[data-variant='multi'][data-is-selected='true'] {
180
+ background: transparent;
181
+ }
182
+
183
+ &[data-variant='single'][data-is-selected='true'] {
184
+ background: ${({ theme }) => parseColor(theme.colors.primary)};
185
+ color: ${({ theme }) => parseColor(theme.colors.white)};
186
+ }
187
+
188
+ &:hover:not([data-disabled='true']):not([data-is-selected='true']:not([data-variant='multi'])) {
189
+ background: ${({ theme }) => parseColor(theme.colors.primaryLight)};
190
+ }
191
+ `
192
+
193
+ export const ItemLabel = styled(Text)`
194
+ padding-left: 4px;
195
+ `;
196
+
197
+
198
+ export const RightIcon = styled.span`
199
+ margin-left: auto;
200
+ padding: ${spacing.spacing8}px;
201
+ display: inline-flex;
202
+ align-items: center;
203
+ justify-content: center;
204
+ color: inherit;
205
+ `
206
+
207
+ export const Checkbox = styled.span<{ $checked: boolean }>`
208
+ width: 16px;
209
+ height: 16px;
210
+ border-radius: 4px;
211
+ border: 1px solid ${({ theme }) => parseColor(theme.colors.neutral400)};
212
+ background: ${({ $checked, theme }) =>
213
+ $checked
214
+ ? parseColor(theme.colors.primary)
215
+ : parseColor(theme.colors.white)};
216
+ display: inline-flex;
217
+ align-items: center;
218
+ justify-content: center;
219
+
220
+ svg {
221
+ color: ${({ theme }) => parseColor(theme.colors.white)};
222
+ }
223
+ `
224
+
225
+ export const Footer = styled.div`
226
+ border-top: 1px solid ${({ theme }) => parseColor(theme.colors.neutral200)};
227
+ padding-top: ${spacing.spacing8}px;
228
+ margin-top: ${spacing.spacing8}px;
229
+ `
230
+
231
+ export const AddNewRow = styled.button`
232
+ width: 100%;
233
+ height: ${spacing.spacing36}px;
234
+ border: 0;
235
+ background: transparent;
236
+ border-radius: 4px;
237
+ display: flex;
238
+ align-items: center;
239
+ gap: ${spacing.spacing8}px;
240
+ padding: 0 ${spacing.spacing12}px;
241
+ cursor: pointer;
242
+ color: ${({ theme }) => parseColor(theme.colors.textDark)};
243
+ font-size: ${fontSize.fontSize14}px;
244
+
245
+ &:hover {
246
+ background: ${({ theme }) => parseColor(theme.colors.primaryLight)};
247
+ }
248
+ `
249
+
250
+ export const Wrapper = styled.div<TStyledInputProps>`
251
+ position: relative;
252
+ width: 100%;
253
+ display: flex;
254
+ flex-direction: column;
255
+ gap: ${spacing.spacing4}px;
256
+ ${({ $themefication, size }) => `
257
+ ${TriggerButton} {
258
+ ${$themefication.input}
259
+ ${size.input}
260
+ }
261
+ ${HelperText} {
262
+ ${$themefication.helperText}
263
+ }
264
+ ${Label} {
265
+ ${$themefication.label}
266
+ ${size.label}
267
+ }
268
+ `}
269
+ `
@@ -0,0 +1,266 @@
1
+ import { useMemo, useState } from 'react'
2
+
3
+ import {
4
+ MagnifyingGlassIcon,
5
+ CheckIcon,
6
+ PlusIcon,
7
+ CaretDownIcon
8
+ } from '@phosphor-icons/react'
9
+
10
+ import Text from '../Text'
11
+ import { ComboboxProps } from './Combobox.types'
12
+
13
+ import RequiredAsterisk from '../RequiredAsterisk'
14
+ import getState from '../TextField/utils/getState'
15
+ import { TextFieldStates } from '../TextField/TextField.states'
16
+ import { textFieldSizes } from '../TextField/TextField.sizes'
17
+
18
+ import * as Popover from '@radix-ui/react-popover'
19
+ import * as s from './Combobox.styles'
20
+ import { isMultiProps, normalizeData } from './utils'
21
+
22
+ const Combobox = (props: ComboboxProps) => {
23
+ const {
24
+ className,
25
+ label,
26
+ helperText,
27
+ error,
28
+ requiredSymbol = false,
29
+ leftIcon,
30
+ items,
31
+ groups,
32
+ onChange,
33
+ placeholder = 'Selecionar',
34
+ searchPlaceholder = 'Pesquisar',
35
+ emptyText = 'Sem resultados.',
36
+ disabled = false,
37
+ width = 280,
38
+ size = 'md',
39
+ onCreate,
40
+ createLabel = 'Adicionar novo'
41
+ } = props
42
+
43
+ const [open, setOpen] = useState(false)
44
+ const { groups: g } = normalizeData(items, groups)
45
+ const state = getState(disabled, !!error)
46
+ const textFieldState = TextFieldStates(state)
47
+ const textFieldSize = textFieldSizes(size, false, true)
48
+
49
+ const isMulti = isMultiProps(props)
50
+ const flatItems = useMemo(() => g.flatMap(x => x.items), [g])
51
+ const selectedValue = isMulti ? undefined : props.value
52
+ const selected = selectedValue
53
+ ? flatItems.find(i => i.value === selectedValue)
54
+ : undefined
55
+ const selectedValues = isMulti ? props.values ?? [] : []
56
+ const selectedLabels = isMulti
57
+ ? flatItems
58
+ .filter(i => selectedValues.includes(i.value))
59
+ .map(i => i.label)
60
+ : []
61
+ const triggerText = isMulti
62
+ ? selectedLabels.length === 0
63
+ ? placeholder
64
+ : selectedLabels.join(', ')
65
+ : selected?.label ?? placeholder
66
+
67
+ const toggle = (value: string) => {
68
+ if (!isMulti) return
69
+ const next = selectedValues.includes(value)
70
+ ? selectedValues.filter(x => x !== value)
71
+ : [...selectedValues, value]
72
+ ;(onChange as ((values: string[]) => void) | undefined)?.(next)
73
+ }
74
+
75
+ return (
76
+ <s.Wrapper
77
+ className={className}
78
+ size={textFieldSize}
79
+ $themefication={textFieldState}>
80
+ {label && (
81
+ <s.Label>
82
+ {label} {requiredSymbol && <RequiredAsterisk />}
83
+ </s.Label>
84
+ )}
85
+ <Popover.Root open={open} onOpenChange={setOpen}>
86
+ <Popover.Trigger asChild>
87
+ <s.TriggerButton
88
+ type="button"
89
+ role="combobox"
90
+ aria-expanded={open}
91
+ aria-invalid={!!error}
92
+ disabled={disabled}
93
+ $width={width}>
94
+ <s.TriggerLabel>
95
+ <Text
96
+ tag="span"
97
+ size="body02"
98
+ color={
99
+ isMulti
100
+ ? selectedLabels.length
101
+ ? 'textDark'
102
+ : 'textMedium'
103
+ : selected
104
+ ? 'textDark'
105
+ : 'textMedium'
106
+ }>
107
+ {triggerText}
108
+ </Text>
109
+ </s.TriggerLabel>
110
+ <s.TriggerIcon aria-hidden>
111
+ <CaretDownIcon weight="fill" size={16} />
112
+ </s.TriggerIcon>
113
+ </s.TriggerButton>
114
+ </Popover.Trigger>
115
+
116
+ <Popover.Portal>
117
+ <s.PopoverContent sideOffset={-1} $width={width} align="start">
118
+ <s.CommandRoot>
119
+ <s.SearchRow>
120
+ <s.SearchIcon>
121
+ <MagnifyingGlassIcon size={16} />
122
+ </s.SearchIcon>
123
+ <s.CommandInput placeholder={searchPlaceholder} />
124
+ </s.SearchRow>
125
+
126
+ <s.List>
127
+ <s.Empty>
128
+ <Text
129
+ tag="span"
130
+ size="body02"
131
+ color="textMedium"
132
+ >
133
+ {emptyText}
134
+ </Text>
135
+ </s.Empty>
136
+
137
+ {g.map((group, idx) => (
138
+ <s.GroupWrap
139
+ key={`${group.heading}-${idx}`}
140
+ heading={group.heading}>
141
+ {group.heading ? (
142
+ <s.GroupHeading>
143
+ <Text
144
+ tag="span"
145
+ size="span01"
146
+ color="textMedium">
147
+ {group.heading}
148
+ </Text>
149
+ </s.GroupHeading>
150
+ ) : null}
151
+
152
+ {group.items.map(item => {
153
+ const isSelected =
154
+ item.value === selectedValue
155
+ const isChecked =
156
+ isMulti &&
157
+ selectedValues.includes(
158
+ item.value
159
+ )
160
+ return (
161
+ <s.ItemRow
162
+ key={item.value}
163
+ value={item.label}
164
+ disabled={item.disabled}
165
+ onSelect={() => {
166
+ if (item.disabled)
167
+ return
168
+ if (isMulti) {
169
+ toggle(item.value)
170
+ return
171
+ }
172
+ ;(
173
+ onChange as
174
+ | ((
175
+ value: string
176
+ ) => void)
177
+ | undefined
178
+ )?.(item.value)
179
+ setOpen(false)
180
+ }}
181
+ data-selected={
182
+ isMulti
183
+ ? isChecked
184
+ ? 'true'
185
+ : 'false'
186
+ : isSelected
187
+ ? 'true'
188
+ : 'false'
189
+ }
190
+ data-is-selected={
191
+ isMulti
192
+ ? isChecked
193
+ ? 'true'
194
+ : 'false'
195
+ : isSelected
196
+ ? 'true'
197
+ : 'false'
198
+ }
199
+ data-variant={
200
+ isMulti ? 'multi' : 'single'
201
+ }>
202
+ {isMulti ? (
203
+ <s.Checkbox
204
+ $checked={
205
+ isChecked
206
+ }>
207
+ {isChecked ? (
208
+ <CheckIcon
209
+ size={12}
210
+ weight="bold"
211
+ />
212
+ ) : null}
213
+ </s.Checkbox>
214
+ ) : null}
215
+ {leftIcon ? leftIcon : null}
216
+ <s.ItemLabel
217
+ tag="span"
218
+ size="body02"
219
+ color={
220
+ !isMulti && isSelected
221
+ ? 'white'
222
+ : 'textDark'
223
+ }>
224
+ {item.label}
225
+ </s.ItemLabel>
226
+ {!isMulti && isSelected && (
227
+ <s.RightIcon>
228
+ <CheckIcon
229
+ size={16}
230
+ weight="bold"
231
+ />
232
+ </s.RightIcon>
233
+ )}
234
+ </s.ItemRow>
235
+ )
236
+ })}
237
+ </s.GroupWrap>
238
+ ))}
239
+ </s.List>
240
+
241
+ {onCreate && (
242
+ <s.Footer>
243
+ <s.AddNewRow
244
+ type="button"
245
+ onClick={() => {
246
+ onCreate()
247
+ }}>
248
+ <PlusIcon size={16} />
249
+ <Text tag="span" size="body02">
250
+ {createLabel}
251
+ </Text>
252
+ </s.AddNewRow>
253
+ </s.Footer>
254
+ )}
255
+ </s.CommandRoot>
256
+ </s.PopoverContent>
257
+ </Popover.Portal>
258
+ </Popover.Root>
259
+ {(helperText || error) && (
260
+ <s.HelperText>{error?.message || helperText}</s.HelperText>
261
+ )}
262
+ </s.Wrapper>
263
+ )
264
+ }
265
+
266
+ export default Combobox
@@ -0,0 +1,45 @@
1
+ import { FieldValues } from 'react-hook-form'
2
+ import React from 'react'
3
+ import { TextFieldSize } from '../TextField/TextField.sizes'
4
+
5
+ export type Item = { value: string; label: string; disabled?: boolean }
6
+ export type Group = { heading: string; items: Item[] }
7
+
8
+ export type BaseProps<TFieldValues extends FieldValues = FieldValues> = {
9
+ className?: string
10
+ label?: string
11
+ helperText?: string
12
+ error?: TFieldValues
13
+ requiredSymbol?: boolean
14
+ leftIcon?: React.ReactNode
15
+ placeholder?: string
16
+ searchPlaceholder?: string
17
+ emptyText?: string
18
+ disabled?: boolean
19
+ width?: number | string
20
+ size?: TextFieldSize
21
+ groups?: Group[]
22
+ items?: Item[]
23
+ onCreate?: () => void
24
+ createLabel?: string
25
+ }
26
+
27
+ export type ComboboxVariant = 'single' | 'multi'
28
+
29
+ export type ComboboxSingleProps<TFieldValues extends FieldValues = FieldValues> =
30
+ BaseProps<TFieldValues> & {
31
+ variant?: 'single'
32
+ value?: string
33
+ onChange?: (value: string) => void
34
+ }
35
+
36
+ export type ComboboxMultiProps<TFieldValues extends FieldValues = FieldValues> =
37
+ BaseProps<TFieldValues> & {
38
+ variant: 'multi'
39
+ values?: string[]
40
+ onChange?: (values: string[]) => void
41
+ }
42
+
43
+ export type ComboboxProps<TFieldValues extends FieldValues = FieldValues> =
44
+ | ComboboxSingleProps<TFieldValues>
45
+ | ComboboxMultiProps<TFieldValues>
@@ -0,0 +1 @@
1
+ export { default as Combobox } from './Combobox'
@@ -0,0 +1,9 @@
1
+ import { ComboboxMultiProps, ComboboxProps, Group, Item } from "../Combobox.types"
2
+
3
+ export const normalizeData = (items?: Item[], groups?: Group[]) => {
4
+ if (groups?.length) return { groups }
5
+ return { groups: items ? [{ heading: '', items }] : [] }
6
+ }
7
+
8
+ export const isMultiProps = (props: ComboboxProps): props is ComboboxMultiProps =>
9
+ props.variant === 'multi'
@@ -10,7 +10,7 @@ const IconButton: React.FC<ButtonProps> = ({
10
10
  className,
11
11
  color = 'primary',
12
12
  disabled,
13
- fluid = false,
13
+ fullWidth = false,
14
14
  size = 'md',
15
15
  onClick,
16
16
  ...rest
@@ -42,7 +42,7 @@ const IconButton: React.FC<ButtonProps> = ({
42
42
  <StyledButton
43
43
  disabled={disabled}
44
44
  className={className}
45
- $fluid={fluid}
45
+ $fullWidth={fullWidth}
46
46
  $variant={buttonVariant}
47
47
  onClick={handleClick}
48
48
  $buttonSize={buttonSize}
@@ -32,7 +32,7 @@ export const StyledLinkButton = styled.button<StyledButtonProps>`
32
32
  display: flex;
33
33
  outline: none !important;
34
34
 
35
- width: ${({ $fluid }) => ($fluid ? '100%' : 'auto')};
35
+ width: ${({ $fullWidth }) => ($fullWidth ? '100%' : 'auto')};
36
36
  ${({ $buttonSize }) => $buttonSize}
37
37
  overflow: hidden;
38
38
  cursor: pointer;
@@ -12,7 +12,7 @@ const LinkButton: React.FC<LinkButtonProps> = ({
12
12
  disabled = false,
13
13
  color = 'primary',
14
14
  variant = 'ghost',
15
- fluid = false,
15
+ fullWidth = false,
16
16
  leftIcon,
17
17
  rightIcon,
18
18
  onClick,
@@ -47,7 +47,7 @@ const LinkButton: React.FC<LinkButtonProps> = ({
47
47
  onClick={handleClick}
48
48
  $variant={buttonVariant}
49
49
  $buttonSize={buttonSize}
50
- $fluid={fluid}
50
+ $fullWidth={fullWidth}
51
51
  aria-disabled={disabled}
52
52
  rel={rest.target === '_blank' ? 'noopener noreferrer' : rest.rel}
53
53
  {...rest}
@@ -6,7 +6,7 @@ export interface LinkButtonProps extends React.AnchorHTMLAttributes<HTMLAnchorEl
6
6
  disabled?: boolean
7
7
  color?: colorType
8
8
  variant?: 'ghost'
9
- fluid?: boolean
9
+ fullWidth?: boolean
10
10
  leftIcon?: React.ReactNode
11
11
  rightIcon?: React.ReactNode
12
12
  size?: 'sm' | 'md' | 'lg'
@@ -18,6 +18,7 @@ export const textFieldSizes = (
18
18
  input: `
19
19
  font-size: ${fontSize.fontSize14}px;
20
20
  line-height: ${lineHeight.lineHeight22}px;
21
+ height: ${spacing.spacing32}px;
21
22
  padding: ${spacing.spacing8}px ${spacing.spacing12}px;
22
23
  padding-left: ${leftIcon ? withIconPadding.sm : spacing.spacing12}px;
23
24
  padding-right: ${rightIcon ? withIconPadding.sm : spacing.spacing12}px;
@@ -32,6 +33,7 @@ export const textFieldSizes = (
32
33
  font-size: ${fontSize.fontSize14}px;
33
34
  line-height: ${lineHeight.lineHeight20}px;
34
35
  padding: ${spacing.spacing12}px ${spacing.spacing16}px;
36
+ height: ${spacing.spacing36}px;
35
37
  padding-left: ${leftIcon ? withIconPadding.md : spacing.spacing16}px;
36
38
  padding-right: ${rightIcon ? withIconPadding.md : spacing.spacing16}px;
37
39
  `,
@@ -43,6 +45,7 @@ export const textFieldSizes = (
43
45
  font-size: ${fontSize.fontSize16}px;
44
46
  line-height: ${lineHeight.lineHeight24}px;
45
47
  padding: ${spacing.spacing12}px ${spacing.spacing16}px;
48
+ height: ${spacing.spacing40}px;
46
49
  padding-left: ${leftIcon ? withIconPadding.lg : spacing.spacing16}px;
47
50
  padding-right: ${rightIcon ? withIconPadding.lg : spacing.spacing16}px;
48
51
  `,
@@ -18,8 +18,9 @@ export const textFieldSizes = (size: TextFieldSize, leftIcon: boolean, rightIcon
18
18
  const sizes = {
19
19
  sm: {
20
20
  input: `
21
- font-size: ${fontSize.fontSize14}px;
22
- line-height: ${lineHeight.lineHeight22}px;
21
+ font-size: ${fontSize.fontSize14}px;
22
+ line-height: ${lineHeight.lineHeight22}px;
23
+ height: ${spacing.spacing32}px;
23
24
  padding: ${spacing.spacing8}px ${spacing.spacing12}px;
24
25
  padding-left: ${leftIcon ? withIconPadding.sm : spacing.spacing12}px;
25
26
  padding-right: ${rightIcon ? withIconPadding.sm : spacing.spacing12}px;
@@ -31,6 +32,7 @@ export const textFieldSizes = (size: TextFieldSize, leftIcon: boolean, rightIcon
31
32
  font-size: ${fontSize.fontSize14}px;
32
33
  line-height: ${lineHeight.lineHeight20}px;
33
34
  padding: ${spacing.spacing12}px ${spacing.spacing16}px;
35
+ height: ${spacing.spacing36}px;
34
36
  padding-left: ${leftIcon ? withIconPadding.md : spacing.spacing16}px;
35
37
  padding-right: ${rightIcon ? withIconPadding.md : spacing.spacing16}px;
36
38
  `,
@@ -41,6 +43,7 @@ export const textFieldSizes = (size: TextFieldSize, leftIcon: boolean, rightIcon
41
43
  font-size: ${fontSize.fontSize16}px;
42
44
  line-height: ${lineHeight.lineHeight24}px;
43
45
  padding: ${spacing.spacing12}px ${spacing.spacing16}px;
46
+ height: ${spacing.spacing40}px;
44
47
  padding-left: ${leftIcon ? withIconPadding.lg : spacing.spacing16}px;
45
48
  padding-right: ${rightIcon ? withIconPadding.lg : spacing.spacing16}px;
46
49
  `,
@@ -5,6 +5,8 @@ export interface StateInterface {
5
5
  input: string
6
6
  label?: string
7
7
  helperText: string
8
+ fileButton?: string
9
+ fileName?: string
8
10
  }
9
11
 
10
12
  interface TextFieldStates {
@@ -27,6 +29,21 @@ export const TextFieldStates = (state: keyof TextFieldStates) => {
27
29
  `,
28
30
  label: `color: ${parseColor(theme.colors.textDark)};`,
29
31
  helperText: `color: ${parseColor(theme.colors.danger200)};`,
32
+ fileButton: `
33
+ border: 1px solid ${parseColor(theme.colors.danger200)};
34
+ background: transparent;
35
+ border-right: none;
36
+ color: ${parseColor(theme.colors.primary)};
37
+ &:hover {
38
+ background: transparent;
39
+ }
40
+ `,
41
+ fileName: `
42
+ border: 1px solid ${parseColor(theme.colors.danger200)};
43
+ color: ${parseColor(theme.colors.textDark)};
44
+ background: ${parseColor(theme.colors.white)};
45
+ border-left: none;
46
+ `,
30
47
  },
31
48
  default: {
32
49
  input: `
@@ -39,6 +56,18 @@ export const TextFieldStates = (state: keyof TextFieldStates) => {
39
56
  `,
40
57
  label: `color: ${parseColor(theme.colors.textDark)};`,
41
58
  helperText: `color: ${parseColor(theme.colors.textMedium)};`,
59
+ fileButton: `
60
+ border: 1px solid ${parseColor(theme.colors.neutral400)};
61
+ background: transparent;
62
+ border-right: none;
63
+ color: ${parseColor(theme.colors.primary)};
64
+ `,
65
+ fileName: `
66
+ border: 1px solid ${parseColor(theme.colors.neutral400)};
67
+ color: ${parseColor(theme.colors.textDark)};
68
+ background: ${parseColor(theme.colors.white)};
69
+ border-left: none;
70
+ `,
42
71
  },
43
72
  disabled: {
44
73
  input: `
@@ -48,6 +77,21 @@ export const TextFieldStates = (state: keyof TextFieldStates) => {
48
77
  `,
49
78
  label: `color: ${parseColor(theme.colors.textDark)};`,
50
79
  helperText: `color: ${parseColor(theme.colors.neutral500)};`,
80
+ fileButton: `
81
+ border: 1px solid ${parseColor(theme.colors.neutral300)};
82
+ background: transparent;
83
+ border-right: none;
84
+ color: ${parseColor(theme.colors.primary)};
85
+ cursor: not-allowed;
86
+ opacity: 0.5;
87
+ `,
88
+ fileName: `
89
+ border: 1px solid ${parseColor(theme.colors.neutral300)};
90
+ background: ${parseColor(theme.colors.neutral50)};
91
+ color: ${parseColor(theme.colors.neutral500)};
92
+ opacity: 0.5;
93
+ border-left: none;
94
+ `,
51
95
  }
52
96
  }
53
97
  return states[state]
@@ -4,6 +4,7 @@ import {
4
4
  fontSize,
5
5
  fontWeight,
6
6
  lineHeight,
7
+ shadow,
7
8
  spacing
8
9
  } from '@liguelead/foundation'
9
10
  import { StateInterface } from './TextField.states'
@@ -14,9 +15,13 @@ interface StyledInputProps {
14
15
  $themefication: StateInterface
15
16
  }
16
17
 
17
- export const InputWrapper = styled.div`
18
+ export const InputWrapper = styled.div<{ $isDragging?: boolean }>`
18
19
  position: relative;
19
20
  width: 100%;
21
+
22
+ box-shadow: ${({ $isDragging }) => ($isDragging ? shadow.focusShadow : 'none')};
23
+ border-radius: 4px;
24
+ background: transparent;
20
25
  `
21
26
 
22
27
  export const Label = styled.label`
@@ -55,15 +60,14 @@ export const StyledInput = styled.input.withConfig({
55
60
  width: 100%;
56
61
  border-radius: 4px;
57
62
  outline: none;
58
- border: 1px solid #CFCFD1;
63
+ border: 1px solid ${parseColor('neutral400')};
59
64
  transition: border-color 0.2s ease;
60
65
  background: transparent;
61
66
  `
62
67
 
63
-
64
68
  export const Wrapper = styled.div<StyledInputProps>`
65
69
  position: relative;
66
- width: 100%;
70
+ max-width: 600px;
67
71
  display: flex;
68
72
  flex-direction: column;
69
73
  gap: ${spacing.spacing4}px;
@@ -79,5 +83,44 @@ export const Wrapper = styled.div<StyledInputProps>`
79
83
  ${HelperText} {
80
84
  ${$themefication.helperText}
81
85
  }
86
+ ${FileButton} {
87
+ ${$themefication.fileButton || $themefication.input}
88
+ ${size.input}
89
+ }
90
+ ${FileName} {
91
+ ${$themefication.fileName || $themefication.input}
92
+ ${size.input}
93
+ }
94
+ ${FileInputContainer} {
95
+ ${StyledInput} {
96
+ ${$themefication.input}
97
+ ${size.input}
98
+ }
99
+ }
82
100
  `}
83
101
  `
102
+
103
+ export const FileButton = styled.span`
104
+ display: inline-flex;
105
+ align-items: center;
106
+ font-weight: 500;
107
+ white-space: nowrap;
108
+ cursor: pointer;
109
+ border-radius: 4px 0 0 4px;
110
+ transition: all 0.2s ease;
111
+ `
112
+
113
+ export const FileName = styled.span`
114
+ flex: 1;
115
+ border-radius: 0 4px 4px 0;
116
+ border-left: none;
117
+ transition: all 0.2s ease;
118
+ display: flex;
119
+ align-items: center;
120
+ `
121
+
122
+ export const FileInputContainer = styled.div`
123
+ display: flex;
124
+ width: 100%;
125
+ position: relative;
126
+ `
@@ -2,6 +2,9 @@ import React, { forwardRef, useState } from 'react'
2
2
  import { TextFieldProps } from './TextField.types'
3
3
  import { StateInterface, TextFieldStates } from './TextField.states'
4
4
  import {
5
+ FileButton,
6
+ FileInputContainer,
7
+ FileName,
5
8
  HelperText,
6
9
  IconWrapper,
7
10
  InputWrapper,
@@ -9,9 +12,10 @@ import {
9
12
  StyledInput,
10
13
  Wrapper
11
14
  } from './TextField.styles'
15
+
12
16
  import { textFieldSizes } from './TextField.sizes'
13
- import { Eye, EyeClosed } from '@phosphor-icons/react'
14
- import getState from './utils/getState'
17
+ import { EyeIcon, EyeClosedIcon } from '@phosphor-icons/react'
18
+ import getState from './utils/getState'
15
19
  import RequiredAsterisk from '../RequiredAsterisk'
16
20
 
17
21
  const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
@@ -23,6 +27,8 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
23
27
  handleLeftIcon,
24
28
  handleRightIcon,
25
29
  helperText,
30
+ filePlaceholder = 'Nenhum arquivo selecionado',
31
+ fileButtonLabel = 'Escolher arquivo',
26
32
  label,
27
33
  leftIcon,
28
34
  onChange,
@@ -37,17 +43,39 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
37
43
  },
38
44
  ref
39
45
  ) => {
40
- const [passwordVisible, setPasswordVisible] = useState(
41
- type !== 'password'
42
- )
46
+ const [passwordVisible, setPasswordVisible] = useState(false)
47
+ const [selectedFileName, setSelectedFileName] = useState('')
43
48
  const state = getState(disabled, !!error)
44
49
  const textFieldState: StateInterface = TextFieldStates(state)
45
50
  const textFieldSize = textFieldSizes(size, !!leftIcon, !!rightIcon)
51
+ const [isDragging, setIsDragging] = useState(false)
52
+
53
+ const getCurrentInputType = () => {
54
+ if (type === 'password') {
55
+ return passwordVisible ? 'text' : 'password'
56
+ }
57
+ return type
58
+ }
46
59
 
47
60
  const togglePasswordVisibility = () => {
48
61
  setPasswordVisible(!passwordVisible)
49
62
  }
50
63
 
64
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
65
+ const files = e.target.files
66
+ if (files && files.length > 0) {
67
+ const fileNames = Array.from(files)
68
+ .map(file => file.name)
69
+ .join(', ')
70
+ setSelectedFileName(fileNames)
71
+ } else {
72
+ setSelectedFileName('')
73
+ }
74
+
75
+ register?.onChange(e)
76
+ onChange?.(e)
77
+ }
78
+
51
79
  const transformRightIcon = (
52
80
  type: string,
53
81
  rightIcon: React.ReactNode
@@ -55,14 +83,97 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
55
83
  if (type === 'password') {
56
84
  return (
57
85
  <IconWrapper onClick={togglePasswordVisibility} $right>
58
- {passwordVisible ? <Eye /> : <EyeClosed />}
86
+ {passwordVisible ? <EyeIcon /> : <EyeClosedIcon />}
87
+ </IconWrapper>
88
+ )
89
+ }
90
+ if (rightIcon) {
91
+ return (
92
+ <IconWrapper onClick={handleRightIcon} $right>
93
+ {rightIcon}
59
94
  </IconWrapper>
60
95
  )
61
96
  }
97
+ return null
98
+ }
99
+
100
+ const handleDrop = (e: React.DragEvent<HTMLLabelElement>) => {
101
+ e.preventDefault()
102
+ e.stopPropagation()
103
+ setIsDragging(false)
104
+
105
+ const files = e.dataTransfer.files
106
+ if (!files || files.length === 0) return
107
+
108
+ const fileNames = Array.from(files)
109
+ .map(f => f.name)
110
+ .join(', ')
111
+ setSelectedFileName(fileNames)
112
+
113
+ const event = {
114
+ target: {
115
+ files
116
+ }
117
+ } as unknown as React.ChangeEvent<HTMLInputElement>
118
+
119
+ register?.onChange(event)
120
+ onChange?.(event)
121
+ }
122
+
123
+ const handleDragOver = (e: React.DragEvent) => {
124
+ e.preventDefault()
125
+ e.stopPropagation()
126
+ setIsDragging(true)
127
+ }
128
+
129
+ const handleDragLeave = (e: React.DragEvent) => {
130
+ e.preventDefault()
131
+ e.stopPropagation()
132
+ setIsDragging(false)
133
+ }
134
+
135
+ if (type === 'file') {
62
136
  return (
63
- <IconWrapper onClick={handleRightIcon} $right>
64
- {rightIcon}
65
- </IconWrapper>
137
+ <Wrapper
138
+ className={className}
139
+ size={textFieldSize}
140
+ $themefication={textFieldState}>
141
+ <Label>
142
+ {label} {requiredSymbol && <RequiredAsterisk />}
143
+ </Label>
144
+ <InputWrapper
145
+ as="label"
146
+ onDragOver={handleDragOver}
147
+ onDragLeave={handleDragLeave}
148
+ onDrop={handleDrop}
149
+ $isDragging={isDragging}
150
+ >
151
+ {leftIcon && (
152
+ <IconWrapper onClick={handleLeftIcon}>
153
+ {leftIcon}
154
+ </IconWrapper>
155
+ )}
156
+ <FileInputContainer>
157
+ <FileButton>{fileButtonLabel}</FileButton>
158
+ <FileName>
159
+ {selectedFileName || filePlaceholder}
160
+ </FileName>
161
+ </FileInputContainer>
162
+ {transformRightIcon(type, rightIcon)}
163
+ <StyledInput
164
+ ref={ref}
165
+ type="file"
166
+ hidden
167
+ disabled={disabled}
168
+ {...props}
169
+ {...register}
170
+ onChange={handleFileChange}
171
+ />
172
+ </InputWrapper>
173
+ {(helperText || error) && (
174
+ <HelperText>{error?.message || helperText}</HelperText>
175
+ )}
176
+ </Wrapper>
66
177
  )
67
178
  }
68
179
 
@@ -70,8 +181,7 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
70
181
  <Wrapper
71
182
  className={className}
72
183
  size={textFieldSize}
73
- $themefication={textFieldState}
74
- >
184
+ $themefication={textFieldState}>
75
185
  <Label>
76
186
  {label} {requiredSymbol && <RequiredAsterisk />}
77
187
  </Label>
@@ -84,7 +194,7 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
84
194
  {transformRightIcon(type, rightIcon)}
85
195
  <StyledInput
86
196
  ref={ref}
87
- type={passwordVisible ? 'text' : 'password'}
197
+ type={getCurrentInputType()}
88
198
  value={value}
89
199
  disabled={disabled}
90
200
  placeholder={placeholder}
@@ -10,6 +10,8 @@ export interface TextFieldProps<TFieldValues extends FieldValues = FieldValues>
10
10
  handleLeftIcon?: () => void
11
11
  handleRightIcon?: () => void
12
12
  helperText?: string
13
+ filePlaceholder?: string
14
+ fileButtonLabel?: string
13
15
  size?: TextFieldSize
14
16
  className?: string
15
17
  placeholder?: string
@@ -18,6 +20,6 @@ export interface TextFieldProps<TFieldValues extends FieldValues = FieldValues>
18
20
  rightIcon?: React.ReactNode
19
21
  error?: TFieldValues
20
22
  requiredSymbol?: boolean
21
- type?: 'text' | 'password' | 'email' | 'number'
23
+ type?: 'text' | 'password' | 'email' | 'number' | 'file'
22
24
  register?: UseFormRegisterReturn<string>
23
25
  }
@@ -15,3 +15,4 @@ export { default as LinkButton } from './LinkButton'
15
15
  export { default as RadioButton } from './RadioButton'
16
16
  export { ToastProvider, Toaster } from './Toaster'
17
17
  export { default as Dialog } from './Dialog'
18
+ export { Combobox } from './Combobox'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liguelead/design-system",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "type": "module",
5
5
  "main": "components/index.ts",
6
6
  "publishConfig": {