@liguelead/design-system 0.0.36 → 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.
Files changed (34) hide show
  1. package/components/Alert/Alert.style.ts +1 -1
  2. package/components/Alert/Alert.tsx +3 -1
  3. package/components/Alert/Alert.variants.ts +2 -4
  4. package/components/Button/Button.appearance.ts +3 -4
  5. package/components/Button/Button.styles.ts +0 -1
  6. package/components/Button/Button.tsx +4 -7
  7. package/components/Button/Button.types.ts +1 -1
  8. package/components/Checkbox/Checkbox.tsx +5 -5
  9. package/components/Combobox/Combobox.styles.ts +1 -1
  10. package/components/Combobox/Combobox.tsx +1 -1
  11. package/components/Dialog/Dialog.style.ts +2 -1
  12. package/components/Dialog/Dialog.tsx +6 -8
  13. package/components/IconButton/IconButton.tsx +3 -1
  14. package/components/LinkButton/LinkButton.tsx +3 -1
  15. package/components/PageWrapper/PageWrapper.tsx +1 -1
  16. package/components/RadioCardGroup/RadioCardGroup.stories.tsx +203 -0
  17. package/components/RadioCardGroup/RadioCardGroup.styles.ts +198 -0
  18. package/components/RadioCardGroup/RadioCardGroup.tsx +159 -0
  19. package/components/RadioCardGroup/RadioCardGroup.types.ts +29 -0
  20. package/components/RadioCardGroup/index.ts +2 -0
  21. package/components/SplitButton/SplitButton.tsx +1 -1
  22. package/components/Stepper/Stepper.appearance.ts +57 -0
  23. package/components/Stepper/Stepper.stories.tsx +300 -0
  24. package/components/Stepper/Stepper.styles.ts +179 -0
  25. package/components/Stepper/Stepper.tsx +118 -0
  26. package/components/Stepper/Stepper.types.ts +27 -0
  27. package/components/Stepper/index.ts +7 -0
  28. package/components/Tabs/Tabs.tsx +2 -0
  29. package/components/TextField/TextField.stories.tsx +109 -2
  30. package/components/TextField/TextField.styles.ts +44 -0
  31. package/components/TextField/TextField.tsx +130 -8
  32. package/components/TextField/TextField.types.ts +11 -1
  33. package/components/Toaster/Toaster.ts +5 -19
  34. package/package.json +1 -1
@@ -0,0 +1,179 @@
1
+ import styled, { css, keyframes } from 'styled-components'
2
+ import { spacing, fontSize, fontWeight, lineHeight, radius } from '@liguelead/foundation'
3
+ import { parseColor } from '../../utils'
4
+ import {
5
+ TStepIndicatorProps,
6
+ TStepSeparatorProps,
7
+ TStepperContainerProps,
8
+ } from './Stepper.types'
9
+ import { StepIndicatorAppearance } from './Stepper.appearance'
10
+
11
+ const popIn = keyframes`
12
+ 0% { transform: scale(0.5); opacity: 0; }
13
+ 60% { transform: scale(1.2); }
14
+ 100% { transform: scale(1); opacity: 1; }
15
+ `
16
+
17
+ const fillLine = keyframes`
18
+ from { width: 0%; }
19
+ to { width: 100%; }
20
+ `
21
+
22
+ const fillLineV = keyframes`
23
+ from { height: 0%; }
24
+ to { height: 100%; }
25
+ `
26
+ export const StepperWrapper = styled.div`
27
+ display: flex;
28
+ flex-direction: row;
29
+ align-items: flex-start;
30
+ width: 100%;
31
+ `
32
+
33
+ export const StepColumn = styled.div`
34
+ display: flex;
35
+ flex-direction: column;
36
+ align-items: center;
37
+ flex-shrink: 0;
38
+ `
39
+
40
+ export const StepperLabelsRow = styled.div`
41
+ display: flex;
42
+ flex-direction: row;
43
+ align-items: flex-start;
44
+ width: 100%;
45
+ `
46
+
47
+ export const StepLabelCell = styled.div`
48
+ display: flex;
49
+ flex-direction: column;
50
+ align-items: center;
51
+ text-align: center;
52
+ margin-top: ${spacing.spacing8}px;
53
+ `
54
+
55
+ export const StepperContainer = styled.ol<TStepperContainerProps>`
56
+ display: flex;
57
+ list-style: none;
58
+ margin: 0;
59
+ padding: 0;
60
+ width: 100%;
61
+ flex-direction: column;
62
+ `
63
+
64
+ export const StepperItem = styled.li<{ $orientation: 'horizontal' | 'vertical' }>`
65
+ display: flex;
66
+ position: relative;
67
+ flex-direction: column;
68
+ align-items: flex-start;
69
+ gap: ${spacing.spacing8}px;
70
+ `
71
+
72
+ export const StepBody = styled.div<{ $orientation: 'horizontal' | 'vertical' }>`
73
+ display: flex;
74
+ flex-direction: row;
75
+ align-items: center;
76
+ gap: ${spacing.spacing8}px;
77
+ `
78
+
79
+ export const StepTrigger = styled.div<{ $orientation: 'horizontal' | 'vertical' }>`
80
+ display: flex;
81
+ align-items: center;
82
+ cursor: default;
83
+ gap: ${spacing.spacing8}px;
84
+ `
85
+
86
+ export const StepIndicator = styled.div<TStepIndicatorProps>`
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ border-radius: 50%;
91
+ flex-shrink: 0;
92
+ font-size: ${fontSize.fontSize14}px;
93
+ font-weight: ${fontWeight.fontWeight600};
94
+ line-height: 1;
95
+ animation: ${popIn} 0.35s ease both;
96
+ transition:
97
+ width 0.3s ease,
98
+ height 0.3s ease,
99
+ background-color 0.3s ease,
100
+ border-color 0.3s ease,
101
+ box-shadow 0.3s ease;
102
+
103
+ ${({ $state }) => StepIndicatorAppearance($state)}
104
+ `
105
+
106
+ export const StepSeparator = styled.div<TStepSeparatorProps>`
107
+ border-radius: ${radius.radius4}px;
108
+ overflow: hidden;
109
+ position: relative;
110
+ background-color: ${({ theme }) => parseColor(theme.colors.neutral400)};
111
+
112
+ ${({ $orientation }) =>
113
+ $orientation === 'vertical'
114
+ ? css`
115
+ width: 2px;
116
+ min-height: ${spacing.spacing8}px;
117
+ margin-left: 13px;
118
+ `
119
+ : css`
120
+ height: 2px;
121
+ flex: 1;
122
+ min-width: ${spacing.spacing16}px;
123
+ margin: 14px ${spacing.spacing4}px 0;
124
+ `}
125
+
126
+ &::after {
127
+ content: '';
128
+ position: absolute;
129
+ inset: 0;
130
+ background-color: ${({ theme }) => parseColor(theme.colors.primary)};
131
+ border-radius: ${radius.radius4}px;
132
+
133
+ ${({ $completed, $orientation }) =>
134
+ $completed
135
+ ? $orientation === 'vertical'
136
+ ? css`
137
+ animation: ${fillLineV} 0.4s ease forwards;
138
+ `
139
+ : css`
140
+ animation: ${fillLine} 0.4s ease forwards;
141
+ `
142
+ : css`
143
+ width: 0%;
144
+ height: 0%;
145
+ `}
146
+ }
147
+ `
148
+
149
+ export const StepContent = styled.div<{ $orientation: 'horizontal' | 'vertical' }>`
150
+ display: flex;
151
+ flex-direction: column;
152
+ align-items: flex-start;
153
+ `
154
+
155
+ export const StepLabel = styled.strong<{ $active: boolean }>`
156
+ font-size: ${fontSize.fontSize14}px;
157
+ font-weight: ${({ $active }) =>
158
+ $active ? fontWeight.fontWeight600 : fontWeight.fontWeight400};
159
+ line-height: ${lineHeight.lineHeight20}px;
160
+ color: ${({ theme, $active }) =>
161
+ $active
162
+ ? parseColor(theme.colors.textDark)
163
+ : parseColor(theme.colors.textMedium)};
164
+ transition: color 0.3s ease;
165
+ `
166
+
167
+ export const StepDescription = styled.p`
168
+ font-size: ${fontSize.fontSize12}px;
169
+ font-weight: ${fontWeight.fontWeight400};
170
+ line-height: ${lineHeight.lineHeight20}px;
171
+ color: ${({ theme }) => parseColor(theme.colors.textMedium)};
172
+ margin: 0;
173
+ `
174
+
175
+ export const StepNumber = styled.span`
176
+ font-size: ${fontSize.fontSize14}px;
177
+ font-weight: ${fontWeight.fontWeight600};
178
+ line-height: 1;
179
+ `
@@ -0,0 +1,118 @@
1
+ import React from 'react'
2
+ import { CheckFatIcon, WarningCircleIcon } from '@phosphor-icons/react'
3
+ import * as S from './Stepper.styles'
4
+ import { TStepperProps, StepState } from './Stepper.types'
5
+
6
+ const getStepState = (index: number, currentStep: number): StepState => {
7
+ if (index < currentStep) return 'completed'
8
+ if (index === currentStep) return 'active'
9
+ return 'pending'
10
+ }
11
+
12
+ const StepIndicatorContent: React.FC<{ state: StepState; index: number }> = ({
13
+ state,
14
+ index,
15
+ }) => {
16
+ if (state === 'completed') return <CheckFatIcon size={14} weight="fill" />
17
+ if (state === 'error') return <WarningCircleIcon size={14} weight="fill" />
18
+ if (state === 'active') return <S.StepNumber>{index + 1}</S.StepNumber>
19
+ return null
20
+ }
21
+
22
+ export const Stepper: React.FC<TStepperProps> = ({
23
+ steps,
24
+ currentStep,
25
+ orientation = 'horizontal',
26
+ className,
27
+ }) => {
28
+ if (orientation === 'horizontal') {
29
+ return (
30
+ <S.StepperWrapper className={className} aria-label="stepper">
31
+ {steps.map((step, index) => {
32
+ const state = getStepState(index, currentStep)
33
+ const isLast = index === steps.length - 1
34
+ return (
35
+ <React.Fragment key={index}>
36
+ <S.StepColumn>
37
+ <S.StepIndicator
38
+ $state={state}
39
+ aria-label={`Etapa ${index + 1}: ${step.label}`}
40
+ aria-current={state === 'active' ? 'step' : undefined}
41
+ >
42
+ <StepIndicatorContent state={state} index={index} />
43
+ </S.StepIndicator>
44
+ <S.StepLabelCell role="listitem" aria-current={state === 'active' ? 'step' : undefined}>
45
+ <S.StepLabel $active={state === 'active'}>
46
+ {step.label}
47
+ </S.StepLabel>
48
+ {step.description && (
49
+ <S.StepDescription>
50
+ {step.description}
51
+ </S.StepDescription>
52
+ )}
53
+ </S.StepLabelCell>
54
+ </S.StepColumn>
55
+ {!isLast && (
56
+ <S.StepSeparator
57
+ role="separator"
58
+ aria-hidden="true"
59
+ $completed={state === 'completed'}
60
+ $orientation="horizontal"
61
+ />
62
+ )}
63
+ </React.Fragment>
64
+ )
65
+ })}
66
+ </S.StepperWrapper>
67
+ )
68
+ }
69
+
70
+ return (
71
+ <S.StepperContainer
72
+ $orientation="vertical"
73
+ className={className}
74
+ role="list"
75
+ aria-label="stepper"
76
+ >
77
+ {steps.map((step, index) => {
78
+ const state = getStepState(index, currentStep)
79
+ const isLast = index === steps.length - 1
80
+ return (
81
+ <S.StepperItem
82
+ key={index}
83
+ $orientation="vertical"
84
+ role="listitem"
85
+ aria-current={state === 'active' ? 'step' : undefined}
86
+ >
87
+ <S.StepBody $orientation="vertical">
88
+ <S.StepIndicator
89
+ $state={state}
90
+ aria-label={`Etapa ${index + 1}: ${step.label}`}
91
+ >
92
+ <StepIndicatorContent state={state} index={index} />
93
+ </S.StepIndicator>
94
+ <S.StepContent $orientation="vertical">
95
+ <S.StepLabel $active={state === 'active'}>
96
+ {step.label}
97
+ </S.StepLabel>
98
+ {step.description && (
99
+ <S.StepDescription>
100
+ {step.description}
101
+ </S.StepDescription>
102
+ )}
103
+ </S.StepContent>
104
+ </S.StepBody>
105
+ {!isLast && (
106
+ <S.StepSeparator
107
+ role="separator"
108
+ aria-hidden="true"
109
+ $completed={state === 'completed'}
110
+ $orientation="vertical"
111
+ />
112
+ )}
113
+ </S.StepperItem>
114
+ )
115
+ })}
116
+ </S.StepperContainer>
117
+ )
118
+ }
@@ -0,0 +1,27 @@
1
+ export type StepperOrientation = 'horizontal' | 'vertical'
2
+ export type StepState = 'completed' | 'active' | 'pending' | 'error'
3
+
4
+ export type TStepItem = {
5
+ label: string
6
+ description?: string
7
+ }
8
+
9
+ export type TStepperProps = {
10
+ steps: TStepItem[]
11
+ currentStep: number
12
+ orientation?: StepperOrientation
13
+ className?: string
14
+ }
15
+
16
+ export type TStepIndicatorProps = {
17
+ $state: StepState
18
+ }
19
+
20
+ export type TStepSeparatorProps = {
21
+ $completed: boolean
22
+ $orientation: StepperOrientation
23
+ }
24
+
25
+ export type TStepperContainerProps = {
26
+ $orientation: StepperOrientation
27
+ }
@@ -0,0 +1,7 @@
1
+ export { Stepper } from './Stepper'
2
+ export type {
3
+ TStepperProps,
4
+ TStepItem,
5
+ StepperOrientation,
6
+ StepState
7
+ } from './Stepper.types'
@@ -15,6 +15,8 @@ const Tabs: React.FC<TabsProps> = ({
15
15
  {items.map(item => (
16
16
  <Button
17
17
  key={item.key}
18
+ aria-controls={`tab-${item.key}`}
19
+ aria-selected={activeKey === item.key}
18
20
  variant={activeKey === item.key ? 'solid' : 'neutralGhost'}
19
21
  color={activeKey === item.key ? 'primary' : undefined}
20
22
  size="sm"
@@ -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
+ `