@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,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
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from './RadioCardGroup'
2
+ export type { RadioCardGroupProps, RadioCardOption } from './RadioCardGroup.types'
@@ -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
+ }