@liguelead/design-system 0.0.32 → 0.0.34

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.
@@ -8,148 +8,173 @@ import {
8
8
  shadow
9
9
  } from '@liguelead/foundation'
10
10
  import { parseColor } from '../../utils'
11
- import { Content, Item } from '@radix-ui/react-select'
11
+ import { Content, Item, ItemIndicator, Trigger } from '@radix-ui/react-select'
12
12
 
13
- interface StyledInputProps {
13
+ interface WrapperProps {
14
14
  size: TextFieldSizeType
15
15
  $themefication: StateInterface
16
16
  }
17
17
 
18
- export const InputWrapper = styled.div`
19
- position: relative;
20
- width: 100%;
21
- `
22
-
23
18
  export const Label = styled.label`
24
19
  display: block;
25
20
  font-weight: ${fontWeight.fontWeight500};
26
21
  font-size: ${fontSize.fontSize14}px;
27
22
  `
28
23
 
29
- export const HelperText = styled.span<{ error?: boolean }>`
24
+ export const HelperText = styled.span`
30
25
  font-size: ${fontSize.fontSize12}px;
31
26
  line-height: ${lineHeight.lineHeight16}px;
32
27
  `
33
28
 
34
- export const IconWrapper = styled.div<{
35
- $right?: boolean;
36
- $error?: boolean;
37
- $disabled?: boolean;
38
- }>` ${({theme, $error, $disabled, $right }) =>`
29
+ export const IconWrapper = styled.div<{
30
+ $right?: boolean
31
+ $error?: boolean
32
+ $disabled?: boolean
33
+ }>`
34
+ ${({ theme, $error, $disabled, $right }) => `
39
35
  display: flex;
40
36
  align-items: center;
41
- justify-content: space-between;
37
+ justify-content: center;
42
38
  position: absolute;
43
- top: 0px;
39
+ top: 0;
44
40
  height: 100%;
45
- cursor: pointer;
46
- ${$right ? 'right: 0px;' : 'left: 0px;'}
47
- padding: 0px ${spacing.spacing16}px;
41
+ pointer-events: ${$right ? 'none' : 'auto'};
42
+ ${$right ? 'right: 0;' : 'left: 0;'}
43
+ padding: 0 ${spacing.spacing16}px;
44
+ z-index: 1;
48
45
 
49
46
  & svg {
50
47
  width: 16px;
51
48
  height: 16px;
52
- fill: ${$error
49
+ fill: ${
50
+ $error
53
51
  ? parseColor(theme.colors.danger200)
54
52
  : $disabled
55
- ? parseColor(theme.colors.neutral400)
56
- : parseColor(theme.colors.neutral800)};
53
+ ? parseColor(theme.colors.neutral400)
54
+ : parseColor(theme.colors.neutral800)
55
+ };
57
56
  transition: fill 0.2s ease;
58
- cursor: ${$disabled ? 'not-allowed' : 'pointer'};
59
57
  }
60
58
  `}
61
59
  `
62
60
 
61
+ export const StyledTrigger = styled(Trigger)`
62
+ position: relative;
63
+ width: 100%;
64
+ display: flex;
65
+ align-items: center;
66
+ text-align: left;
67
+ background: transparent;
68
+ outline: none;
69
+ cursor: pointer;
70
+ border-radius: 4px;
71
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
72
+
73
+ &:focus-visible {
74
+ box-shadow: ${shadow.focusShadow};
75
+ }
76
+
77
+ &[data-state='open'] {
78
+ box-shadow: ${shadow.focusShadow};
79
+ }
80
+
81
+ &[data-disabled] {
82
+ cursor: not-allowed;
83
+ }
84
+
85
+ &::placeholder {
86
+ color: ${({ theme }) => parseColor(theme.colors.neutral600)};
87
+ opacity: 1;
88
+ }
89
+ `
90
+
91
+ export const TriggerValue = styled.span<{
92
+ $hasValue: boolean
93
+ $leftIcon: boolean
94
+ $rightIcon: boolean
95
+ }>`
96
+ ${({ theme, $hasValue, $leftIcon, $rightIcon }) => `
97
+ flex: 1;
98
+ overflow: hidden;
99
+ white-space: nowrap;
100
+ text-overflow: ellipsis;
101
+ color: ${$hasValue ? parseColor(theme.colors.textDark) : parseColor(theme.colors.neutral600)};
102
+ padding-left: ${$leftIcon ? '0' : '0'};
103
+ padding-right: ${$rightIcon ? '0' : '0'};
104
+ `}
105
+ `
106
+
63
107
  export const StyledContent = styled(Content)`
64
- padding: 0px;
108
+ padding: 0;
65
109
  background-color: #fff;
66
110
  min-width: var(--radix-select-trigger-width, auto);
67
- width: 100%;
111
+ width: var(--radix-select-trigger-width, 100%);
68
112
  box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
69
113
  border: 1px solid ${({ theme }) => parseColor(theme.colors.neutral500)};
70
- border-top: 0px;
71
- max-height: var(--radix-select-content-height, 314px);
72
- overflow-y: scroll !important;
73
- z-index: 10;
74
-
75
- & .RadixSelectViewport {
76
- max-height: 200px; /* Limite de altura do dropdown */
77
- overflow-y: auto; /* Adiciona o scroll quando necessário */
78
- scrollbar-width: thin; /* Personaliza a largura do scroll (opcional) */
114
+ border-top: 0;
115
+ border-radius: 0 0 8px 8px;
116
+ max-height: 314px;
117
+ overflow: hidden;
118
+ z-index: 9999;
119
+
120
+ &[data-state='open'] {
121
+ animation: none;
79
122
  }
80
123
  `
81
124
 
125
+ export const StyledViewport = styled.div`
126
+ max-height: 200px;
127
+ overflow-y: auto;
128
+ scrollbar-width: thin;
129
+ border-radius: 0 0 4px 4px;
130
+ `
131
+
82
132
  export const StyledItem = styled(Item)`
83
- padding: ${spacing.spacing12}px ${spacing.spacing20}px;
133
+ padding: ${spacing.spacing8}px ${spacing.spacing12}px;
84
134
  display: flex;
85
135
  align-items: center;
86
136
  justify-content: space-between;
87
- gap: 8px; /* mantém espaçamento interno caso adicionemos ícones à esquerda futuramente */
88
- transition: all 0.2s ease;
137
+ gap: 8px;
138
+ transition: background-color 0.2s ease;
89
139
  cursor: pointer;
140
+ outline: none;
141
+ font-size: ${fontSize.fontSize14}px;
90
142
 
91
- &:hover {
143
+ &:hover,
144
+ &[data-highlighted] {
92
145
  background-color: ${({ theme }) => parseColor(theme.colors.primaryLight)};
93
146
  }
94
147
 
95
148
  &[data-state='checked'] {
96
- background-color: ${({ theme }) =>
97
- parseColor(theme.colors.primary)};
149
+ background-color: ${({ theme }) => parseColor(theme.colors.primary)};
98
150
  color: ${({ theme }) => parseColor(theme.colors.white)};
99
151
  }
100
- `
101
-
102
- export const StyledInput = styled.input.withConfig({
103
- shouldForwardProp: prop => !['control', 'errors', 'rules'].includes(prop)
104
- })`
105
- width: 100%;
106
- border-radius: 4px;
107
- outline: none;
108
- transition: border-color 0.2s ease;
109
- background: transparent;
110
- cursor: pointer;
111
-
112
- &:focus {
113
- box-shadow: ${shadow.focusShadow}
114
- }
115
152
 
116
- &.is-open {
117
- border-color: ${({ theme }) =>
118
- parseColor(theme.colors.neutral800)} !important;
119
- box-shadow: ${shadow.focusShadow}
120
- }
121
-
122
- &:disabled {
153
+ &[data-disabled] {
154
+ opacity: 0.5;
123
155
  cursor: not-allowed;
124
156
  }
125
157
  `
126
158
 
127
- export const BorderWrapper = styled.div`
128
- position: absolute;
129
- top: 0;
130
- left: 0;
131
- right: 0;
132
- bottom: 0;
133
- pointer-events: none;
134
- border: 2px solid #ccc;
135
- border-radius: 4px;
136
- background-color: transparent;
137
- z-index: -1;
159
+ export const StyledItemIndicator = styled(ItemIndicator)`
160
+ display: flex;
161
+ align-items: center;
162
+ `
138
163
 
139
- ${StyledInput}:focus ~ & {
140
- border-color: ${({ theme }) => theme.colors.primary};
141
- }
164
+ export const InputWrapper = styled.div`
165
+ position: relative;
166
+ width: 100%;
142
167
  `
143
168
 
144
- export const Wrapper = styled.div<StyledInputProps>`
169
+ export const Wrapper = styled.div<WrapperProps>`
145
170
  position: relative;
146
171
  width: 100%;
147
172
  display: flex;
148
173
  flex-direction: column;
149
174
  gap: ${spacing.spacing4}px;
150
- cursor: pointer;
175
+
151
176
  ${({ $themefication, size }) => `
152
- ${StyledInput} {
177
+ ${StyledTrigger} {
153
178
  ${$themefication.input}
154
179
  ${size.input}
155
180
  }
@@ -1,4 +1,3 @@
1
- import { forwardRef, useState } from 'react'
2
1
  import * as RadixSelect from '@radix-ui/react-select'
3
2
  import { CaretDownIcon, CheckIcon } from '@phosphor-icons/react'
4
3
  import {
@@ -6,152 +5,111 @@ import {
6
5
  IconWrapper,
7
6
  InputWrapper,
8
7
  StyledContent,
9
- StyledInput,
10
8
  StyledItem,
11
- Wrapper,
12
- Label
9
+ StyledItemIndicator,
10
+ StyledTrigger,
11
+ StyledViewport,
12
+ TriggerValue,
13
+ Wrapper
13
14
  } from './Select.styles'
14
15
  import { SelectStates } from './Select.states'
15
16
  import { textFieldSizes } from './Select.sizes'
16
17
  import { SelectProps, StateInterface } from './Select.types'
17
18
  import getState from '../TextField/utils/getState'
18
19
  import RequiredAsterisk from '../RequiredAsterisk'
20
+ import { Label } from './Select.styles'
19
21
 
20
- const Select = forwardRef<HTMLInputElement, SelectProps>(
21
- (
22
- {
23
- label,
24
- defaultValue,
25
- error,
26
- helperText,
27
- leftIcon,
28
- size = 'md',
29
- options,
30
- placeholder,
31
- className,
32
- disabled = false,
33
- requiredSymbol = false,
34
- handleLeftIcon,
35
- register
36
- },
37
- ref
38
- ) => {
39
- const [selectValue, setSelectValue] = useState(
40
- defaultValue ?? options[0]?.value
41
- )
42
- const [open, setOpen] = useState(false)
22
+ const Select = ({
23
+ label,
24
+ defaultValue,
25
+ value,
26
+ error,
27
+ helperText,
28
+ leftIcon,
29
+ size = 'md',
30
+ options,
31
+ placeholder,
32
+ className,
33
+ disabled = false,
34
+ requiredSymbol = false,
35
+ handleLeftIcon,
36
+ onValueChange
37
+ }: SelectProps) => {
38
+ const state = getState(disabled, !!error)
39
+ const textFieldState: StateInterface = SelectStates(state)
40
+ const textFieldSize = textFieldSizes(size, !!leftIcon, true)
43
41
 
44
- const handleOnChange = (value: string) => {
45
- setSelectValue(value)
46
- register?.onChange({
47
- target: {
48
- name: register.name,
49
- value
50
- }
51
- })
52
- }
42
+ const isControlled = value !== undefined
43
+ const rootProps = isControlled
44
+ ? { value: value ?? '', onValueChange }
45
+ : { defaultValue, onValueChange }
53
46
 
54
- const state = getState(disabled, !!error)
55
- const textFieldState: StateInterface = SelectStates(state)
56
- const textFieldSize = textFieldSizes(size, !!leftIcon, false)
57
- const selectedLabel =
58
- options.find(option => option.value === selectValue)?.label ?? ''
59
-
60
- return (
61
- <Wrapper
62
- className={className}
63
- size={textFieldSize}
64
- $themefication={textFieldState}
65
- >
66
- <input
67
- type="hidden"
68
- value={selectValue}
69
- placeholder={placeholder}
70
- ref={instance => {
71
- if (typeof ref === 'function') {
72
- ref(instance)
73
- } else if (ref) {
74
- ref.current = instance
75
- }
76
- register?.ref(instance)
77
- }}
78
- onChange={register?.onChange}
79
- onBlur={register?.onBlur}
80
- />
47
+ return (
48
+ <Wrapper className={className} size={textFieldSize} $themefication={textFieldState}>
49
+ {label && (
81
50
  <Label>
82
51
  {label} {requiredSymbol && <RequiredAsterisk />}
83
52
  </Label>
84
- {disabled ? (
85
- <InputWrapper className="is-disabled">
53
+ )}
54
+
55
+ <RadixSelect.Root {...rootProps} disabled={disabled}>
56
+ <InputWrapper>
57
+ <StyledTrigger aria-label={label} aria-invalid={!!error}>
86
58
  {leftIcon && (
87
- <IconWrapper onClick={handleLeftIcon} $disabled={disabled}>
59
+ <IconWrapper
60
+ onClick={handleLeftIcon}
61
+ $disabled={disabled}
62
+ $error={!!error}
63
+ >
88
64
  {leftIcon}
89
65
  </IconWrapper>
90
66
  )}
67
+
68
+ <TriggerValue
69
+ $hasValue={isControlled ? !!value : true}
70
+ $leftIcon={!!leftIcon}
71
+ $rightIcon
72
+ >
73
+ <RadixSelect.Value placeholder={placeholder} />
74
+ </TriggerValue>
75
+
91
76
  <IconWrapper $right $error={!!error} $disabled={disabled}>
92
- <CaretDownIcon weight="fill" size={14} />
77
+ <RadixSelect.Icon asChild>
78
+ <CaretDownIcon weight="fill" size={14} />
79
+ </RadixSelect.Icon>
93
80
  </IconWrapper>
94
- <StyledInput
95
- disabled
96
- readOnly
97
- value={selectedLabel}
98
- className="is-disabled"
99
- />
100
- </InputWrapper>
101
- ) : (
102
- <RadixSelect.Root
103
- value={selectValue}
104
- onOpenChange={open => setOpen(open)}
105
- defaultValue={defaultValue}
106
- onValueChange={(value: string) =>
107
- handleOnChange(value)
108
- }>
109
- <RadixSelect.Trigger asChild disabled={disabled}>
110
- <InputWrapper>
111
- {leftIcon && (
112
- <IconWrapper onClick={handleLeftIcon}>
113
- {leftIcon}
114
- </IconWrapper>
115
- )}
116
- <IconWrapper $right $error={!!error}>
117
- <CaretDownIcon weight="fill" size={14} />
118
- </IconWrapper>
119
- <StyledInput
120
- readOnly
121
- style={{ cursor: 'pointer !important' }}
122
- value={selectedLabel}
123
- className={open ? 'is-open' : ''}
124
- />
125
- </InputWrapper>
126
- </RadixSelect.Trigger>
127
- <StyledContent
128
- asChild
129
- sideOffset={0}
130
- position="popper"
131
- align="start"
132
- className="RadixSelectContent">
133
- <RadixSelect.Viewport className="RadixSelectViewport">
81
+ </StyledTrigger>
82
+ </InputWrapper>
83
+
84
+ <RadixSelect.Portal>
85
+ <StyledContent
86
+ sideOffset={0}
87
+ position="popper"
88
+ align="start"
89
+ >
90
+ <RadixSelect.Viewport asChild>
91
+ <StyledViewport>
134
92
  {options.map(option => (
135
- <StyledItem
136
- key={option.value}
137
- value={option.value}
138
- >
139
- <span>{option.label}</span>
140
- {option.value === selectValue && (
141
- <CheckIcon weight="bold" />
142
- )}
93
+ <StyledItem key={option.value} value={option.value}>
94
+ <RadixSelect.ItemText>
95
+ {option.label}
96
+ </RadixSelect.ItemText>
97
+ <StyledItemIndicator>
98
+ <CheckIcon weight="bold" size={14} />
99
+ </StyledItemIndicator>
143
100
  </StyledItem>
144
101
  ))}
145
- </RadixSelect.Viewport>
146
- </StyledContent>
147
- </RadixSelect.Root>
148
- )}
149
- {(helperText || error) && (
150
- <HelperText>{error?.message || helperText}</HelperText>
151
- )}
152
- </Wrapper>
153
- )
154
- }
155
- )
102
+ </StyledViewport>
103
+ </RadixSelect.Viewport>
104
+ </StyledContent>
105
+ </RadixSelect.Portal>
106
+ </RadixSelect.Root>
107
+
108
+ {(helperText || error) && (
109
+ <HelperText>{error?.message || helperText}</HelperText>
110
+ )}
111
+ </Wrapper>
112
+ )
113
+ }
156
114
 
157
115
  export default Select
@@ -1,20 +1,19 @@
1
- import { FieldValues, UseFormRegisterReturn } from 'react-hook-form'
1
+ import { FieldValues } from 'react-hook-form'
2
2
  export interface SelectProps<TFieldValues extends FieldValues = FieldValues> {
3
3
  className?: string
4
4
  disabled?: boolean
5
5
  error?: TFieldValues
6
6
  label?: string
7
7
  placeholder?: string
8
- requiredSymbol? :boolean
8
+ requiredSymbol?: boolean
9
9
  options: Array<{ label: string; value: string }>
10
10
  value?: string
11
11
  defaultValue?: string
12
12
  handleLeftIcon?: () => void
13
13
  helperText?: string
14
14
  leftIcon?: React.ReactNode
15
- onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
15
+ onValueChange?: (value: string) => void
16
16
  size?: 'sm' | 'md' | 'lg'
17
- register?: UseFormRegisterReturn<string>
18
17
  }
19
18
 
20
19
  export interface StateInterface {
@@ -34,7 +34,6 @@ interface DatatableColumnFilterMenuProps {
34
34
  labels?: DatatableFilterDropdownLabels
35
35
  }
36
36
 
37
- const SELECT_PLACEHOLDER_VALUE = '__placeholder__'
38
37
 
39
38
  function DatatableColumnFilterMenu({
40
39
  columnId,
@@ -50,13 +49,10 @@ function DatatableColumnFilterMenu({
50
49
  const filterType = meta?.filterType
51
50
  const filterLabel = meta?.filterLabel ?? String(title)
52
51
  const filterPlaceholder = meta?.filterPlaceholder ?? 'Pesquisar...'
53
- const selectOptions = [
54
- { label: filterPlaceholder, value: SELECT_PLACEHOLDER_VALUE },
55
- ...(meta?.selectOptions ?? []).map((o) => ({
56
- label: o.label,
57
- value: String(o.value),
58
- })),
59
- ]
52
+ const selectOptions = (meta?.selectOptions ?? []).map((o) => ({
53
+ label: o.label,
54
+ value: String(o.value),
55
+ }))
60
56
 
61
57
  const hasActiveFilter =
62
58
  filterValue !== undefined &&
@@ -92,11 +88,7 @@ function DatatableColumnFilterMenu({
92
88
  }
93
89
 
94
90
  const handleSelectChange = (value: string) => {
95
- if (!value || value === SELECT_PLACEHOLDER_VALUE) {
96
- onFilterValueChange(columnId, undefined)
97
- } else {
98
- onFilterValueChange(columnId, { type: 'select', value })
99
- }
91
+ onFilterValueChange(columnId, { type: 'select', value })
100
92
  onOpenChange(false)
101
93
  }
102
94
 
@@ -109,7 +101,7 @@ function DatatableColumnFilterMenu({
109
101
  const selectValue =
110
102
  filterValue?.type === 'select' && filterValue.value != null
111
103
  ? String(filterValue.value)
112
- : SELECT_PLACEHOLDER_VALUE
104
+ : undefined
113
105
 
114
106
  if (!filterType) {
115
107
  return (
@@ -203,19 +195,13 @@ function DatatableColumnFilterMenu({
203
195
 
204
196
  {filterType === 'select' && (
205
197
  <Select
206
- key={`select-${columnId}-${selectValue}`}
198
+ key={`select-${columnId}`}
207
199
  label=""
208
200
  options={selectOptions}
209
- defaultValue={selectValue}
201
+ value={selectValue ?? ''}
202
+ placeholder={filterPlaceholder}
210
203
  size="sm"
211
- register={{
212
- name: `datatable-select-${columnId}`,
213
- ref: () => { },
214
- onBlur: async () => { },
215
- onChange: async (e: { target: { value: string } }) => {
216
- handleSelectChange(e.target.value)
217
- },
218
- }}
204
+ onValueChange={handleSelectChange}
219
205
  />
220
206
  )}
221
207
  </FilterPopoverContent>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liguelead/design-system",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "type": "module",
5
5
  "main": "components/index.ts",
6
6
  "publishConfig": {
@@ -13,7 +13,7 @@
13
13
  "react-hook-form": ">=7.0.0"
14
14
  },
15
15
  "dependencies": {
16
- "@liguelead/foundation": "^0.0.16",
16
+ "@liguelead/foundation": "^0.0.17",
17
17
  "@phosphor-icons/react": "^2.1.7",
18
18
  "@radix-ui/react-dropdown-menu": "^2.1.6",
19
19
  "react-day-picker": "^9.12.0",