@liguelead/design-system 0.0.26 → 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
  `,
@@ -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'
@@ -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.26",
3
+ "version": "0.0.27",
4
4
  "type": "module",
5
5
  "main": "components/index.ts",
6
6
  "publishConfig": {