@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.
- package/components/Alert/Alert.style.ts +1 -1
- package/components/Alert/Alert.tsx +3 -1
- package/components/Alert/Alert.variants.ts +2 -4
- package/components/Button/Button.appearance.ts +3 -4
- package/components/Button/Button.styles.ts +0 -1
- package/components/Button/Button.tsx +4 -7
- package/components/Button/Button.types.ts +1 -1
- package/components/Checkbox/Checkbox.tsx +5 -5
- package/components/Combobox/Combobox.styles.ts +1 -1
- package/components/Combobox/Combobox.tsx +1 -1
- package/components/Dialog/Dialog.style.ts +2 -1
- package/components/Dialog/Dialog.tsx +6 -8
- package/components/IconButton/IconButton.tsx +3 -1
- package/components/LinkButton/LinkButton.tsx +3 -1
- package/components/PageWrapper/PageWrapper.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/SplitButton/SplitButton.tsx +1 -1
- 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/Tabs/Tabs.tsx +2 -0
- package/components/TextField/TextField.stories.tsx +109 -2
- package/components/TextField/TextField.styles.ts +44 -0
- package/components/TextField/TextField.tsx +130 -8
- package/components/TextField/TextField.types.ts +11 -1
- package/components/Toaster/Toaster.ts +5 -19
- package/package.json +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import { Badge } from '../Badge'
|
|
3
|
+
import { RadioCardGroupProps } from './RadioCardGroup.types'
|
|
4
|
+
import {
|
|
5
|
+
GroupWrapper,
|
|
6
|
+
CardWrapper,
|
|
7
|
+
CardRadioRow,
|
|
8
|
+
RadioCircle,
|
|
9
|
+
CardTitleRow,
|
|
10
|
+
CardIconWrapper,
|
|
11
|
+
Divider,
|
|
12
|
+
CardContent,
|
|
13
|
+
DescriptionWrapper,
|
|
14
|
+
DescriptionText,
|
|
15
|
+
SeeMoreButton,
|
|
16
|
+
BadgesSection,
|
|
17
|
+
BadgesLabel,
|
|
18
|
+
BadgesList,
|
|
19
|
+
} from './RadioCardGroup.styles'
|
|
20
|
+
import Text from '../Text'
|
|
21
|
+
|
|
22
|
+
const DEFAULT_MAX_LINES = 3
|
|
23
|
+
|
|
24
|
+
interface DescriptionWithToggleProps {
|
|
25
|
+
text: string
|
|
26
|
+
maxLines?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DescriptionWithToggle = ({ text, maxLines = DEFAULT_MAX_LINES }: DescriptionWithToggleProps) => {
|
|
30
|
+
const [expanded, setExpanded] = useState(false)
|
|
31
|
+
const [isClamped, setIsClamped] = useState(false)
|
|
32
|
+
const ref = useRef<HTMLParagraphElement>(null)
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const el = ref.current
|
|
36
|
+
if (!el) return
|
|
37
|
+
setIsClamped(el.scrollHeight > el.clientHeight + 1)
|
|
38
|
+
}, [text, maxLines])
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<DescriptionWrapper $expanded={expanded} $maxLines={maxLines}>
|
|
43
|
+
<DescriptionText ref={ref}>{text}</DescriptionText>
|
|
44
|
+
</DescriptionWrapper>
|
|
45
|
+
{(isClamped || expanded) && (
|
|
46
|
+
<SeeMoreButton
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={(e) => {
|
|
49
|
+
e.preventDefault()
|
|
50
|
+
e.stopPropagation()
|
|
51
|
+
setExpanded(v => !v)
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{expanded ? 'Ver menos' : 'Ver mais'}
|
|
55
|
+
</SeeMoreButton>
|
|
56
|
+
)}
|
|
57
|
+
</>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const RadioCardGroup = <TFieldValues extends object = object>({
|
|
62
|
+
name,
|
|
63
|
+
options,
|
|
64
|
+
value,
|
|
65
|
+
onChange,
|
|
66
|
+
disabled = false,
|
|
67
|
+
className,
|
|
68
|
+
error,
|
|
69
|
+
columns,
|
|
70
|
+
minCardWidth,
|
|
71
|
+
maxCardWidth,
|
|
72
|
+
scrollable,
|
|
73
|
+
maxHeight,
|
|
74
|
+
...rest
|
|
75
|
+
}: RadioCardGroupProps<TFieldValues>) => {
|
|
76
|
+
const hasError = !!error
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<GroupWrapper
|
|
80
|
+
className={className}
|
|
81
|
+
$columns={columns}
|
|
82
|
+
$minCardWidth={minCardWidth}
|
|
83
|
+
$scrollable={scrollable}
|
|
84
|
+
$maxHeight={maxHeight}
|
|
85
|
+
{...rest}
|
|
86
|
+
>
|
|
87
|
+
{options.map((option) => {
|
|
88
|
+
const isChecked = value === option.value
|
|
89
|
+
const isDisabled = disabled || !!option.disabled
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<CardWrapper
|
|
93
|
+
key={option.value}
|
|
94
|
+
$checked={isChecked}
|
|
95
|
+
$disabled={isDisabled}
|
|
96
|
+
$error={hasError}
|
|
97
|
+
$columns={columns}
|
|
98
|
+
$minCardWidth={minCardWidth}
|
|
99
|
+
$maxCardWidth={maxCardWidth}
|
|
100
|
+
data-disabled={isDisabled}
|
|
101
|
+
>
|
|
102
|
+
<CardRadioRow>
|
|
103
|
+
<RadioCircle
|
|
104
|
+
type="radio"
|
|
105
|
+
name={name}
|
|
106
|
+
value={option.value}
|
|
107
|
+
checked={isChecked}
|
|
108
|
+
disabled={isDisabled}
|
|
109
|
+
onChange={(e) => {
|
|
110
|
+
option.register?.onChange(e)
|
|
111
|
+
onChange?.(option.value)
|
|
112
|
+
}}
|
|
113
|
+
{...option.register}
|
|
114
|
+
/>
|
|
115
|
+
<CardTitleRow>
|
|
116
|
+
{option.icon && (
|
|
117
|
+
<CardIconWrapper>{option.icon}</CardIconWrapper>
|
|
118
|
+
)}
|
|
119
|
+
<Text color="textDark" weight="fontWeight500" size="body02" tag="p">
|
|
120
|
+
{option.label}
|
|
121
|
+
</Text>
|
|
122
|
+
</CardTitleRow>
|
|
123
|
+
</CardRadioRow>
|
|
124
|
+
|
|
125
|
+
{(option.description || (option.badges && option.badges.length > 0)) && (
|
|
126
|
+
<>
|
|
127
|
+
<Divider />
|
|
128
|
+
<CardContent>
|
|
129
|
+
{option.description && (
|
|
130
|
+
<DescriptionWithToggle
|
|
131
|
+
text={option.description}
|
|
132
|
+
maxLines={option.descriptionMaxLines}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
{option.badges && option.badges.length > 0 && (
|
|
136
|
+
<BadgesSection>
|
|
137
|
+
{option.badgesLabel && (
|
|
138
|
+
<BadgesLabel>{option.badgesLabel}</BadgesLabel>
|
|
139
|
+
)}
|
|
140
|
+
<BadgesList>
|
|
141
|
+
{option.badges.map((badge, i) => (
|
|
142
|
+
<Badge key={i} color="primary">
|
|
143
|
+
{badge}
|
|
144
|
+
</Badge>
|
|
145
|
+
))}
|
|
146
|
+
</BadgesList>
|
|
147
|
+
</BadgesSection>
|
|
148
|
+
)}
|
|
149
|
+
</CardContent>
|
|
150
|
+
</>
|
|
151
|
+
)}
|
|
152
|
+
</CardWrapper>
|
|
153
|
+
)
|
|
154
|
+
})}
|
|
155
|
+
</GroupWrapper>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default RadioCardGroup
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { FieldValues, UseFormRegisterReturn } from 'react-hook-form'
|
|
3
|
+
|
|
4
|
+
export interface RadioCardOption {
|
|
5
|
+
value: string
|
|
6
|
+
label: string
|
|
7
|
+
icon?: React.ReactNode
|
|
8
|
+
description?: string
|
|
9
|
+
descriptionMaxLines?: number
|
|
10
|
+
badges?: string[]
|
|
11
|
+
badgesLabel?: string
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
register?: UseFormRegisterReturn<string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RadioCardGroupProps<TFieldValues extends FieldValues = FieldValues>
|
|
17
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
18
|
+
name: string
|
|
19
|
+
options: RadioCardOption[]
|
|
20
|
+
value?: string
|
|
21
|
+
onChange?: (value: string) => void
|
|
22
|
+
disabled?: boolean
|
|
23
|
+
error?: TFieldValues
|
|
24
|
+
columns?: number
|
|
25
|
+
minCardWidth?: string
|
|
26
|
+
maxCardWidth?: string
|
|
27
|
+
scrollable?: boolean
|
|
28
|
+
maxHeight?: string
|
|
29
|
+
}
|
|
@@ -25,7 +25,7 @@ const SplitButton: React.FC<SplitButtonProps> = ({
|
|
|
25
25
|
className
|
|
26
26
|
}) => {
|
|
27
27
|
const theme = useTheme()
|
|
28
|
-
const buttonVariant = ButtonVariant(color, variant)
|
|
28
|
+
const buttonVariant = ButtonVariant(color, variant, theme)
|
|
29
29
|
const buttonSize = ButtonSizes(size)
|
|
30
30
|
const triggerSize = SplitButtonTriggerSizes(size)
|
|
31
31
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { css } from 'styled-components'
|
|
2
|
+
import { useTheme } from 'styled-components'
|
|
3
|
+
import { parseColor } from '../../utils'
|
|
4
|
+
import { StepState } from './Stepper.types'
|
|
5
|
+
|
|
6
|
+
export const StepIndicatorAppearance = ($state: StepState) => {
|
|
7
|
+
const theme = useTheme()
|
|
8
|
+
const colors = theme.colors
|
|
9
|
+
|
|
10
|
+
if ($state === 'completed') {
|
|
11
|
+
return css`
|
|
12
|
+
width: 28px;
|
|
13
|
+
height: 28px;
|
|
14
|
+
background-color: ${parseColor(colors.primary)};
|
|
15
|
+
border: 2px solid ${parseColor(colors.primary)};
|
|
16
|
+
color: ${parseColor(colors.white)};
|
|
17
|
+
`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if ($state === 'active') {
|
|
21
|
+
return css`
|
|
22
|
+
width: 28px;
|
|
23
|
+
height: 28px;
|
|
24
|
+
background-color: transparent;
|
|
25
|
+
border: 2.5px solid ${parseColor(colors.primary)};
|
|
26
|
+
color: ${parseColor(colors.primary)};
|
|
27
|
+
box-shadow: 0 0 0 4px ${parseColor(colors.primary)}22;
|
|
28
|
+
`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if ($state === 'error') {
|
|
32
|
+
return css`
|
|
33
|
+
width: 28px;
|
|
34
|
+
height: 28px;
|
|
35
|
+
background-color: transparent;
|
|
36
|
+
border: 2px solid ${parseColor(colors.danger200)};
|
|
37
|
+
color: ${parseColor(colors.danger200)};
|
|
38
|
+
`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return css`
|
|
42
|
+
width: 28px;
|
|
43
|
+
height: 28px;
|
|
44
|
+
background-color: transparent;
|
|
45
|
+
border: none;
|
|
46
|
+
color: transparent;
|
|
47
|
+
|
|
48
|
+
&::before {
|
|
49
|
+
content: '';
|
|
50
|
+
display: block;
|
|
51
|
+
width: 10px;
|
|
52
|
+
height: 10px;
|
|
53
|
+
border-radius: 50%;
|
|
54
|
+
background-color: ${parseColor(colors.neutral400)};
|
|
55
|
+
}
|
|
56
|
+
`
|
|
57
|
+
}
|
|
@@ -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
|
+
}
|