@liguelead/design-system 0.0.37 → 0.0.38
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.
- package/components/Button/Button.types.ts +1 -1
- package/components/Combobox/Combobox.styles.ts +1 -1
- package/components/Combobox/Combobox.tsx +1 -1
- package/components/RadioCardGroup/RadioCardGroup.stories.tsx +203 -0
- package/components/RadioCardGroup/RadioCardGroup.styles.ts +198 -0
- package/components/RadioCardGroup/RadioCardGroup.tsx +159 -0
- package/components/RadioCardGroup/RadioCardGroup.types.ts +29 -0
- package/components/RadioCardGroup/index.ts +2 -0
- package/components/Stepper/Stepper.appearance.ts +57 -0
- package/components/Stepper/Stepper.stories.tsx +300 -0
- package/components/Stepper/Stepper.styles.ts +179 -0
- package/components/Stepper/Stepper.tsx +118 -0
- package/components/Stepper/Stepper.types.ts +27 -0
- package/components/Stepper/index.ts +7 -0
- package/components/TextField/TextField.stories.tsx +109 -2
- package/components/TextField/TextField.styles.ts +44 -0
- package/components/TextField/TextField.tsx +118 -1
- package/components/TextField/TextField.types.ts +11 -1
- package/package.json +1 -1
|
@@ -15,13 +15,14 @@ Campo de texto para entrada de dados pelo usuário.
|
|
|
15
15
|
|
|
16
16
|
**Tamanhos:** \`sm\` · \`md\` · \`lg\`
|
|
17
17
|
|
|
18
|
-
**Tipos suportados:** \`text\` · \`password\` · \`email\` · \`number\` · \`file\`
|
|
18
|
+
**Tipos suportados:** \`text\` · \`password\` · \`email\` · \`number\` · \`file\` · \`textarea\`
|
|
19
19
|
|
|
20
20
|
**Boas práticas:**
|
|
21
21
|
- Sempre use \`label\` para identificar o campo
|
|
22
22
|
- Use \`helperText\` para instruções ou validações
|
|
23
23
|
- Use \`error\` para comunicar erros de validação
|
|
24
24
|
- Ícones devem reforçar o tipo de dado esperado
|
|
25
|
+
- No \`textarea\`, use \`maxLength\` para o contador visual e \`htmlMaxLength\` para bloquear digitação pelo browser
|
|
25
26
|
`,
|
|
26
27
|
},
|
|
27
28
|
},
|
|
@@ -48,7 +49,7 @@ Campo de texto para entrada de dados pelo usuário.
|
|
|
48
49
|
},
|
|
49
50
|
type: {
|
|
50
51
|
control: 'select',
|
|
51
|
-
options: ['text', 'password', 'email', 'number', 'file'],
|
|
52
|
+
options: ['text', 'password', 'email', 'number', 'file', 'textarea'],
|
|
52
53
|
description: 'Tipo do input HTML',
|
|
53
54
|
table: { defaultValue: { summary: 'text' } },
|
|
54
55
|
},
|
|
@@ -180,6 +181,112 @@ export const Obrigatorio: Story = {
|
|
|
180
181
|
},
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
export const TextArea: Story = {
|
|
185
|
+
name: 'TextArea',
|
|
186
|
+
parameters: {
|
|
187
|
+
docs: {
|
|
188
|
+
description: {
|
|
189
|
+
story: 'Variante textarea com contador de caracteres, botão de variável e controle de maxLength.',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
render: () => (
|
|
194
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 24, width: 480 }}>
|
|
195
|
+
<TextField
|
|
196
|
+
label="Mensagem simples"
|
|
197
|
+
type="textarea"
|
|
198
|
+
placeholder="Digite sua mensagem..."
|
|
199
|
+
maxLength={160}
|
|
200
|
+
/>
|
|
201
|
+
<TextField
|
|
202
|
+
label="Com botão de variável"
|
|
203
|
+
type="textarea"
|
|
204
|
+
placeholder="Digite sua mensagem..."
|
|
205
|
+
maxLength={160}
|
|
206
|
+
showVariableButton
|
|
207
|
+
variableButtonLabel="Inserir variável"
|
|
208
|
+
/>
|
|
209
|
+
<TextField
|
|
210
|
+
label="Com bloqueio nativo (htmlMaxLength)"
|
|
211
|
+
type="textarea"
|
|
212
|
+
placeholder="Máximo 60 caracteres — browser bloqueia ao atingir o limite"
|
|
213
|
+
maxLength={60}
|
|
214
|
+
htmlMaxLength
|
|
215
|
+
showVariableButton
|
|
216
|
+
variableButtonLabel="Inserir variável"
|
|
217
|
+
/>
|
|
218
|
+
<TextField
|
|
219
|
+
label="Desabilitado"
|
|
220
|
+
type="textarea"
|
|
221
|
+
placeholder="Campo desabilitado"
|
|
222
|
+
maxLength={160}
|
|
223
|
+
showVariableButton
|
|
224
|
+
disabled
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const TextAreaHtmlMaxLength: Story = {
|
|
231
|
+
name: 'TextArea — htmlMaxLength',
|
|
232
|
+
parameters: {
|
|
233
|
+
docs: {
|
|
234
|
+
description: {
|
|
235
|
+
story: `
|
|
236
|
+
Por padrão \`maxLength\` só exibe o contador visual — o usuário pode digitar além do limite (estado de erro).
|
|
237
|
+
|
|
238
|
+
Com \`htmlMaxLength\` o browser bloqueia a digitação ao atingir o limite. O botão "Inserir variável" também respeita o limite.
|
|
239
|
+
|
|
240
|
+
| Prop | Comportamento |
|
|
241
|
+
|------|--------------|
|
|
242
|
+
| \`maxLength={n}\` | Contador visual, sem bloqueio |
|
|
243
|
+
| \`maxLength={n} htmlMaxLength\` | Contador + bloqueio nativo |
|
|
244
|
+
`,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
render: () => (
|
|
249
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 24, width: 480 }}>
|
|
250
|
+
<TextField
|
|
251
|
+
label="Só contador (sem bloqueio)"
|
|
252
|
+
type="textarea"
|
|
253
|
+
placeholder="Pode digitar além de 30..."
|
|
254
|
+
maxLength={30}
|
|
255
|
+
showVariableButton
|
|
256
|
+
variableButtonLabel="Inserir variável"
|
|
257
|
+
/>
|
|
258
|
+
<TextField
|
|
259
|
+
label="Contador + bloqueio nativo"
|
|
260
|
+
type="textarea"
|
|
261
|
+
placeholder="Bloqueado em 30 caracteres"
|
|
262
|
+
maxLength={30}
|
|
263
|
+
htmlMaxLength
|
|
264
|
+
showVariableButton
|
|
265
|
+
variableButtonLabel="Inserir variável"
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const TextAreaComErro: Story = {
|
|
272
|
+
name: 'TextArea com erro',
|
|
273
|
+
parameters: {
|
|
274
|
+
docs: { description: { story: 'Estado de erro no textarea.' } },
|
|
275
|
+
},
|
|
276
|
+
render: () => (
|
|
277
|
+
<div style={{ width: 480 }}>
|
|
278
|
+
<TextField
|
|
279
|
+
label="Mensagem"
|
|
280
|
+
type="textarea"
|
|
281
|
+
placeholder="Digite sua mensagem..."
|
|
282
|
+
maxLength={160}
|
|
283
|
+
showVariableButton
|
|
284
|
+
error={{ message: 'Campo obrigatório.' }}
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
),
|
|
288
|
+
}
|
|
289
|
+
|
|
183
290
|
export const ExemploLogin: Story = {
|
|
184
291
|
name: 'Exemplo: Formulário de login',
|
|
185
292
|
parameters: {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
fontSize,
|
|
5
5
|
fontWeight,
|
|
6
6
|
lineHeight,
|
|
7
|
+
radius,
|
|
7
8
|
shadow,
|
|
8
9
|
spacing
|
|
9
10
|
} from '@liguelead/foundation'
|
|
@@ -76,6 +77,9 @@ export const Wrapper = styled.div<StyledInputProps>`
|
|
|
76
77
|
${$themefication.input}
|
|
77
78
|
${size.input}
|
|
78
79
|
}
|
|
80
|
+
${StyledTextArea} {
|
|
81
|
+
${$themefication.input}
|
|
82
|
+
}
|
|
79
83
|
${Label} {
|
|
80
84
|
${$themefication.label}
|
|
81
85
|
${size.label}
|
|
@@ -124,3 +128,43 @@ export const FileInputContainer = styled.div`
|
|
|
124
128
|
width: 100%;
|
|
125
129
|
position: relative;
|
|
126
130
|
`
|
|
131
|
+
|
|
132
|
+
export const StyledTextArea = styled.textarea.withConfig({
|
|
133
|
+
shouldForwardProp: prop => !['control', 'errors', 'rules'].includes(prop)
|
|
134
|
+
})`
|
|
135
|
+
width: 100%;
|
|
136
|
+
border-radius: ${radius.radius4}px;
|
|
137
|
+
outline: none;
|
|
138
|
+
border: 1px solid ${parseColor('neutral400')};
|
|
139
|
+
transition: border-color 0.2s ease;
|
|
140
|
+
background: transparent;
|
|
141
|
+
resize: vertical;
|
|
142
|
+
font-family: inherit;
|
|
143
|
+
font-size: ${fontSize.fontSize14}px;
|
|
144
|
+
line-height: ${lineHeight.lineHeight22}px;
|
|
145
|
+
padding: ${spacing.spacing12}px ${spacing.spacing16}px;
|
|
146
|
+
color: inherit;
|
|
147
|
+
`
|
|
148
|
+
|
|
149
|
+
export const TextAreaWrapper = styled.div`
|
|
150
|
+
position: relative;
|
|
151
|
+
width: 100%;
|
|
152
|
+
border-radius: ${radius.radius4}px;
|
|
153
|
+
background: transparent;
|
|
154
|
+
`
|
|
155
|
+
|
|
156
|
+
export const TextAreaFooter = styled.div`
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
justify-content: space-between;
|
|
160
|
+
margin-top: -6px;
|
|
161
|
+
`
|
|
162
|
+
|
|
163
|
+
export const CharCount = styled.span<{ $isOver?: boolean }>`
|
|
164
|
+
font-size: ${fontSize.fontSize12}px;
|
|
165
|
+
line-height: ${lineHeight.lineHeight16}px;
|
|
166
|
+
color: ${({ theme, $isOver }) =>
|
|
167
|
+
$isOver
|
|
168
|
+
? parseColor(theme.colors.danger200)
|
|
169
|
+
: parseColor(theme.colors.textMedium)};
|
|
170
|
+
`
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { forwardRef, useState } from 'react'
|
|
1
|
+
import React, { forwardRef, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { TextFieldProps } from './TextField.types'
|
|
3
3
|
import { StateInterface, TextFieldStates } from './TextField.states'
|
|
4
4
|
import {
|
|
5
|
+
CharCount,
|
|
5
6
|
FileButton,
|
|
6
7
|
FileInputContainer,
|
|
7
8
|
FileName,
|
|
@@ -10,6 +11,9 @@ import {
|
|
|
10
11
|
InputWrapper,
|
|
11
12
|
Label,
|
|
12
13
|
StyledInput,
|
|
14
|
+
StyledTextArea,
|
|
15
|
+
TextAreaFooter,
|
|
16
|
+
TextAreaWrapper,
|
|
13
17
|
Wrapper
|
|
14
18
|
} from './TextField.styles'
|
|
15
19
|
|
|
@@ -17,6 +21,7 @@ import { textFieldSizes } from './TextField.sizes'
|
|
|
17
21
|
import { EyeIcon, EyeClosedIcon } from '@phosphor-icons/react'
|
|
18
22
|
import getState from './utils/getState'
|
|
19
23
|
import RequiredAsterisk from '../RequiredAsterisk'
|
|
24
|
+
import Button from '../Button'
|
|
20
25
|
|
|
21
26
|
const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
22
27
|
(
|
|
@@ -39,12 +44,22 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
39
44
|
register,
|
|
40
45
|
requiredSymbol = false,
|
|
41
46
|
placeholder,
|
|
47
|
+
showVariableButton = false,
|
|
48
|
+
variableButtonLabel = 'Variável',
|
|
49
|
+
maxLength,
|
|
50
|
+
htmlMaxLength = false,
|
|
51
|
+
onTextAreaChange,
|
|
42
52
|
...props
|
|
43
53
|
},
|
|
44
54
|
ref
|
|
45
55
|
) => {
|
|
46
56
|
const [passwordVisible, setPasswordVisible] = useState(false)
|
|
47
57
|
const [selectedFileName, setSelectedFileName] = useState('')
|
|
58
|
+
const [textAreaValue, setTextAreaValue] = useState(
|
|
59
|
+
(value as string) ?? ''
|
|
60
|
+
)
|
|
61
|
+
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
|
62
|
+
const varCountRef = useRef(0)
|
|
48
63
|
const state = getState(disabled, !!error)
|
|
49
64
|
const textFieldState: StateInterface = TextFieldStates(state)
|
|
50
65
|
const textFieldSize = textFieldSizes(size, !!leftIcon, !!rightIcon)
|
|
@@ -131,6 +146,108 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
131
146
|
e.stopPropagation()
|
|
132
147
|
setIsDragging(false)
|
|
133
148
|
}
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (value !== undefined) {
|
|
151
|
+
setTextAreaValue(value as string)
|
|
152
|
+
}
|
|
153
|
+
}, [value])
|
|
154
|
+
|
|
155
|
+
const handleInsertVariable = () => {
|
|
156
|
+
const el = textAreaRef.current
|
|
157
|
+
if (!el) return
|
|
158
|
+
|
|
159
|
+
const matches = textAreaValue.match(/\{\{(\d+)\}\}/g) ?? []
|
|
160
|
+
const usedNumbers = matches.map(m => parseInt(m.replace(/\D/g, ''), 10))
|
|
161
|
+
let next = 1
|
|
162
|
+
while (usedNumbers.includes(next)) next++
|
|
163
|
+
varCountRef.current = next
|
|
164
|
+
const variable = `{{${next}}}`
|
|
165
|
+
const start = el.selectionStart ?? textAreaValue.length
|
|
166
|
+
const end = el.selectionEnd ?? textAreaValue.length
|
|
167
|
+
const newValue =
|
|
168
|
+
textAreaValue.slice(0, start) +
|
|
169
|
+
variable +
|
|
170
|
+
textAreaValue.slice(end)
|
|
171
|
+
|
|
172
|
+
if (maxLength !== undefined && newValue.length > maxLength) return
|
|
173
|
+
|
|
174
|
+
setTextAreaValue(newValue)
|
|
175
|
+
|
|
176
|
+
requestAnimationFrame(() => {
|
|
177
|
+
el.focus()
|
|
178
|
+
el.setSelectionRange(
|
|
179
|
+
start + variable.length,
|
|
180
|
+
start + variable.length
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const handleTextAreaChange = (
|
|
186
|
+
e: React.ChangeEvent<HTMLTextAreaElement>
|
|
187
|
+
) => {
|
|
188
|
+
setTextAreaValue(e.target.value)
|
|
189
|
+
onTextAreaChange?.(e)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (type === 'textarea') {
|
|
193
|
+
const charCount = textAreaValue.length
|
|
194
|
+
const isOver = maxLength !== undefined && charCount > maxLength
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<Wrapper
|
|
198
|
+
className={className}
|
|
199
|
+
size={textFieldSize}
|
|
200
|
+
$themefication={textFieldState}
|
|
201
|
+
>
|
|
202
|
+
{label && (
|
|
203
|
+
<Label>
|
|
204
|
+
{label}{' '}
|
|
205
|
+
{requiredSymbol && <RequiredAsterisk />}
|
|
206
|
+
</Label>
|
|
207
|
+
)}
|
|
208
|
+
<TextAreaWrapper>
|
|
209
|
+
<StyledTextArea
|
|
210
|
+
ref={textAreaRef}
|
|
211
|
+
value={textAreaValue}
|
|
212
|
+
disabled={disabled}
|
|
213
|
+
placeholder={placeholder}
|
|
214
|
+
maxLength={htmlMaxLength ? maxLength : undefined}
|
|
215
|
+
onChange={handleTextAreaChange}
|
|
216
|
+
aria-invalid={!!error}
|
|
217
|
+
aria-describedby={
|
|
218
|
+
helperText || error
|
|
219
|
+
? `${label}-helper`
|
|
220
|
+
: undefined
|
|
221
|
+
}
|
|
222
|
+
/>
|
|
223
|
+
</TextAreaWrapper>
|
|
224
|
+
<TextAreaFooter>
|
|
225
|
+
{maxLength !== undefined && (
|
|
226
|
+
<CharCount $isOver={isOver}>
|
|
227
|
+
{charCount}/{maxLength}
|
|
228
|
+
</CharCount>
|
|
229
|
+
)}
|
|
230
|
+
{showVariableButton && (
|
|
231
|
+
<Button
|
|
232
|
+
size="sm"
|
|
233
|
+
variant="ghost"
|
|
234
|
+
type="button"
|
|
235
|
+
disabled={disabled}
|
|
236
|
+
onClick={handleInsertVariable}
|
|
237
|
+
aria-label={variableButtonLabel}
|
|
238
|
+
>
|
|
239
|
+
{variableButtonLabel}
|
|
240
|
+
</Button>
|
|
241
|
+
)}
|
|
242
|
+
</TextAreaFooter>
|
|
243
|
+
{(helperText || error) && (
|
|
244
|
+
<HelperText id={`${label}-helper`}>
|
|
245
|
+
{error?.message || helperText}
|
|
246
|
+
</HelperText>
|
|
247
|
+
)}
|
|
248
|
+
</Wrapper>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
134
251
|
|
|
135
252
|
if (type === 'file') {
|
|
136
253
|
return (
|
|
@@ -20,6 +20,16 @@ export interface TextFieldProps<TFieldValues extends FieldValues = FieldValues>
|
|
|
20
20
|
rightIcon?: React.ReactNode
|
|
21
21
|
error?: TFieldValues
|
|
22
22
|
requiredSymbol?: boolean
|
|
23
|
-
type?: 'text' | 'password' | 'email' | 'number' | 'file'
|
|
23
|
+
type?: 'text' | 'password' | 'email' | 'number' | 'file' | 'textarea'
|
|
24
24
|
register?: UseFormRegisterReturn<string>
|
|
25
|
+
/** Habilita o botão de inserir variável (apenas para type="textarea") */
|
|
26
|
+
showVariableButton?: boolean
|
|
27
|
+
/** Label do botão de variável */
|
|
28
|
+
variableButtonLabel?: string
|
|
29
|
+
/** Limite máximo de caracteres (exibido no rodapé quando type="textarea") */
|
|
30
|
+
maxLength?: number
|
|
31
|
+
/** Passa o maxLength nativo ao <textarea>, bloqueando digitação pelo browser. Default: false */
|
|
32
|
+
htmlMaxLength?: boolean
|
|
33
|
+
/** Callback chamado quando o valor do textarea muda */
|
|
34
|
+
onTextAreaChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
|
25
35
|
}
|