@liguelead/design-system 0.0.37 → 0.0.39
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 +199 -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 +119 -1
- package/components/TextField/TextField.types.ts +11 -1
- package/package.json +1 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
3
|
+
import { Stepper } from './Stepper'
|
|
4
|
+
import Button from '../Button'
|
|
5
|
+
|
|
6
|
+
const steps = [
|
|
7
|
+
{ label: 'Seus dados', description: 'Nome e e-mail' },
|
|
8
|
+
{ label: 'Empresa', description: 'Dados da empresa' },
|
|
9
|
+
{ label: 'Convidar time', description: 'Colabore com sua equipe' },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof Stepper> = {
|
|
13
|
+
title: 'Navigation/Stepper',
|
|
14
|
+
component: Stepper,
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: 'padded',
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component: `
|
|
20
|
+
Componente Stepper para indicar progresso em processos multi-etapas.
|
|
21
|
+
|
|
22
|
+
**Orientações:**
|
|
23
|
+
- \`horizontal\` — padrão, ideal para fluxos com poucas etapas
|
|
24
|
+
- \`vertical\` — ideal para etapas com descrições longas ou em painéis laterais
|
|
25
|
+
|
|
26
|
+
**Controle de estado:**
|
|
27
|
+
- O prop \`currentStep\` (0-indexed) define qual etapa está ativa
|
|
28
|
+
- Etapas anteriores ao \`currentStep\` são marcadas como concluídas automaticamente
|
|
29
|
+
`,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
tags: ['autodocs'],
|
|
34
|
+
argTypes: {
|
|
35
|
+
orientation: {
|
|
36
|
+
control: 'select',
|
|
37
|
+
options: ['horizontal', 'vertical'],
|
|
38
|
+
description: 'Orientação do stepper',
|
|
39
|
+
table: { defaultValue: { summary: 'horizontal' } },
|
|
40
|
+
},
|
|
41
|
+
currentStep: {
|
|
42
|
+
control: { type: 'number', min: 0, max: 2 },
|
|
43
|
+
description: 'Índice da etapa atual (0-indexed)',
|
|
44
|
+
},
|
|
45
|
+
steps: {
|
|
46
|
+
control: false,
|
|
47
|
+
description: 'Array de etapas com label e description opcional',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default meta
|
|
53
|
+
type Story = StoryObj<typeof meta>
|
|
54
|
+
|
|
55
|
+
export const Default: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
steps,
|
|
58
|
+
currentStep: 1,
|
|
59
|
+
orientation: 'horizontal',
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const Horizontal: Story = {
|
|
64
|
+
name: 'Horizontal',
|
|
65
|
+
parameters: {
|
|
66
|
+
docs: {
|
|
67
|
+
description: { story: 'Orientação padrão, ideal para fluxos lineares.' },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
render: () => {
|
|
71
|
+
const [current, setCurrent] = useState(0)
|
|
72
|
+
return (
|
|
73
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
|
74
|
+
<Stepper steps={steps} currentStep={current} orientation="horizontal" />
|
|
75
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
76
|
+
<Button
|
|
77
|
+
variant="outline"
|
|
78
|
+
size="sm"
|
|
79
|
+
onClick={() => setCurrent(c => Math.max(0, c - 1))}
|
|
80
|
+
disabled={current === 0}
|
|
81
|
+
>
|
|
82
|
+
Anterior
|
|
83
|
+
</Button>
|
|
84
|
+
<Button
|
|
85
|
+
size="sm"
|
|
86
|
+
onClick={() => setCurrent(c => Math.min(steps.length - 1, c + 1))}
|
|
87
|
+
disabled={current === steps.length - 1}
|
|
88
|
+
>
|
|
89
|
+
Próximo
|
|
90
|
+
</Button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const Vertical: Story = {
|
|
98
|
+
name: 'Vertical',
|
|
99
|
+
parameters: {
|
|
100
|
+
docs: {
|
|
101
|
+
description: { story: 'Orientação vertical, ideal para painéis laterais ou etapas com descrições longas.' },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
render: () => {
|
|
105
|
+
const [current, setCurrent] = useState(0)
|
|
106
|
+
return (
|
|
107
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 24, maxWidth: 320 }}>
|
|
108
|
+
<Stepper steps={steps} currentStep={current} orientation="vertical" />
|
|
109
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
110
|
+
<Button
|
|
111
|
+
variant="outline"
|
|
112
|
+
size="sm"
|
|
113
|
+
onClick={() => setCurrent(c => Math.max(0, c - 1))}
|
|
114
|
+
disabled={current === 0}
|
|
115
|
+
>
|
|
116
|
+
Anterior
|
|
117
|
+
</Button>
|
|
118
|
+
<Button
|
|
119
|
+
size="sm"
|
|
120
|
+
onClick={() => setCurrent(c => Math.min(steps.length - 1, c + 1))}
|
|
121
|
+
disabled={current === steps.length - 1}
|
|
122
|
+
>
|
|
123
|
+
Próximo
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const SemDescricao: Story = {
|
|
132
|
+
name: 'Sem descrição',
|
|
133
|
+
parameters: {
|
|
134
|
+
docs: {
|
|
135
|
+
description: { story: 'Stepper apenas com labels, sem descrições.' },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
args: {
|
|
139
|
+
steps: [
|
|
140
|
+
{ label: 'Etapa 1' },
|
|
141
|
+
{ label: 'Etapa 2' },
|
|
142
|
+
{ label: 'Etapa 3' },
|
|
143
|
+
{ label: 'Etapa 4' },
|
|
144
|
+
],
|
|
145
|
+
currentStep: 2,
|
|
146
|
+
orientation: 'horizontal',
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const PrimeiraEtapa: Story = {
|
|
151
|
+
name: 'Primeira etapa',
|
|
152
|
+
args: {
|
|
153
|
+
steps,
|
|
154
|
+
currentStep: 0,
|
|
155
|
+
orientation: 'horizontal',
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const UltimaEtapa: Story = {
|
|
160
|
+
name: 'Última etapa',
|
|
161
|
+
args: {
|
|
162
|
+
steps,
|
|
163
|
+
currentStep: steps.length - 1,
|
|
164
|
+
orientation: 'horizontal',
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Form multi-step example ────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
import { useForm } from 'react-hook-form'
|
|
171
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
172
|
+
import { z } from 'zod'
|
|
173
|
+
import TextField from '../TextField'
|
|
174
|
+
|
|
175
|
+
const formSteps = [
|
|
176
|
+
{ label: 'Seus dados', description: 'Nome e e-mail' },
|
|
177
|
+
{ label: 'Empresa', description: 'Dados da empresa' },
|
|
178
|
+
{ label: 'Senha', description: 'Crie sua senha' },
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
const formSchema = z
|
|
182
|
+
.object({
|
|
183
|
+
name: z.string().min(3, 'Nome deve ter ao menos 3 caracteres'),
|
|
184
|
+
email: z.string().email('E-mail inválido'),
|
|
185
|
+
company: z.string().min(2, 'Nome da empresa obrigatório'),
|
|
186
|
+
role: z.string().min(2, 'Cargo obrigatório'),
|
|
187
|
+
password: z.string().min(6, 'Mínimo 6 caracteres'),
|
|
188
|
+
confirm: z.string(),
|
|
189
|
+
})
|
|
190
|
+
.refine(d => d.password === d.confirm, {
|
|
191
|
+
message: 'As senhas não coincidem',
|
|
192
|
+
path: ['confirm'],
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
type FormData = z.infer<typeof formSchema>
|
|
196
|
+
|
|
197
|
+
const stepFields: (keyof FormData)[][] = [
|
|
198
|
+
['name', 'email'],
|
|
199
|
+
['company', 'role'],
|
|
200
|
+
['password', 'confirm'],
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
type StepFormProps = {
|
|
204
|
+
form: ReturnType<typeof useForm<FormData>>
|
|
205
|
+
onNext: () => void
|
|
206
|
+
onBack?: () => void
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const StepForm0: React.FC<StepFormProps> = ({ form, onNext }) => {
|
|
210
|
+
const { register, formState: { errors }, trigger } = form
|
|
211
|
+
const handleNext = async () => {
|
|
212
|
+
if (await trigger(stepFields[0])) onNext()
|
|
213
|
+
}
|
|
214
|
+
return (
|
|
215
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
216
|
+
<TextField label="Nome completo" register={register('name')} error={errors.name} requiredSymbol />
|
|
217
|
+
<TextField label="E-mail" type="email" register={register('email')} error={errors.email} requiredSymbol />
|
|
218
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
219
|
+
<Button size="sm" onClick={handleNext}>Próximo</Button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const StepForm1: React.FC<StepFormProps> = ({ form, onNext, onBack }) => {
|
|
226
|
+
const { register, formState: { errors }, trigger } = form
|
|
227
|
+
const handleNext = async () => {
|
|
228
|
+
if (await trigger(stepFields[1])) onNext()
|
|
229
|
+
}
|
|
230
|
+
return (
|
|
231
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
232
|
+
<TextField label="Empresa" register={register('company')} error={errors.company} requiredSymbol />
|
|
233
|
+
<TextField label="Cargo" register={register('role')} error={errors.role} requiredSymbol />
|
|
234
|
+
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
|
235
|
+
<Button size="sm" variant="outline" onClick={onBack}>Anterior</Button>
|
|
236
|
+
<Button size="sm" onClick={handleNext}>Próximo</Button>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const StepForm2: React.FC<StepFormProps> = ({ form, onNext, onBack }) => {
|
|
243
|
+
const { register, formState: { errors }, trigger } = form
|
|
244
|
+
const handleNext = async () => {
|
|
245
|
+
if (await trigger(stepFields[2])) onNext()
|
|
246
|
+
}
|
|
247
|
+
return (
|
|
248
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
249
|
+
<TextField label="Senha" type="password" register={register('password')} error={errors.password} requiredSymbol />
|
|
250
|
+
<TextField label="Confirmar senha" type="password" register={register('confirm')} error={errors.confirm} requiredSymbol />
|
|
251
|
+
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
|
252
|
+
<Button size="sm" variant="outline" onClick={onBack}>Anterior</Button>
|
|
253
|
+
<Button size="sm" onClick={handleNext}>Finalizar</Button>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const FormMultiStep: Story = {
|
|
260
|
+
name: 'Formulário multi-step',
|
|
261
|
+
parameters: {
|
|
262
|
+
docs: {
|
|
263
|
+
description: {
|
|
264
|
+
story: 'Dados preservados ao voltar entre etapas. Um único `useForm` no pai com `trigger` parcial por step.',
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
render: () => {
|
|
269
|
+
const [current, setCurrent] = useState(0)
|
|
270
|
+
const [done, setDone] = useState(false)
|
|
271
|
+
|
|
272
|
+
const form = useForm<FormData>({
|
|
273
|
+
resolver: zodResolver(formSchema),
|
|
274
|
+
mode: 'onTouched',
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const next = () => setCurrent(c => c + 1)
|
|
278
|
+
const back = () => setCurrent(c => c - 1)
|
|
279
|
+
const finish = () => setDone(true)
|
|
280
|
+
const reset = () => { setCurrent(0); setDone(false); form.reset() }
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 24, maxWidth: 480 }}>
|
|
284
|
+
<Stepper steps={formSteps} currentStep={done ? formSteps.length : current} orientation="horizontal" />
|
|
285
|
+
{done ? (
|
|
286
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'center', padding: 24 }}>
|
|
287
|
+
<p style={{ margin: 0, fontWeight: 600 }}>Cadastro concluído!</p>
|
|
288
|
+
<Button size="sm" variant="outline" onClick={reset}>Recomeçar</Button>
|
|
289
|
+
</div>
|
|
290
|
+
) : (
|
|
291
|
+
<>
|
|
292
|
+
{current === 0 && <StepForm0 form={form} onNext={next} />}
|
|
293
|
+
{current === 1 && <StepForm1 form={form} onNext={next} onBack={back} />}
|
|
294
|
+
{current === 2 && <StepForm2 form={form} onNext={finish} onBack={back} />}
|
|
295
|
+
</>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
)
|
|
299
|
+
},
|
|
300
|
+
}
|
|
@@ -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
|
+
}
|