@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
@@ -13,7 +13,7 @@ export const AlertContainer = styled.div<{ $variant: string }>`
13
13
 
14
14
  export const AlertContent = styled.div`
15
15
  display: flex;
16
- gap: 8px;
16
+ gap: ${spacing.spacing8}px;
17
17
  `;
18
18
 
19
19
  export const AlertButtonContainer = styled.div`
@@ -1,3 +1,4 @@
1
+ import { useTheme } from 'styled-components'
1
2
  import Text from '../Text'
2
3
  import LinkButton from '../LinkButton'
3
4
 
@@ -21,7 +22,8 @@ const Alert = ({
21
22
  title,
22
23
  hasButton
23
24
  }: TAlertProps) => {
24
- const alertVariant = AlertVariant(variant)
25
+ const theme = useTheme()
26
+ const alertVariant = AlertVariant(variant, theme)
25
27
 
26
28
  return (
27
29
  <AlertContainer $variant={alertVariant} className={className}>
@@ -1,11 +1,9 @@
1
1
  import { parseColor } from '../../utils'
2
- import { useTheme } from 'styled-components'
2
+ import { DefaultTheme } from 'styled-components'
3
3
 
4
4
  type TVariant = 'success' | 'danger' | 'warning' | 'info' | 'default'
5
5
 
6
- export const AlertVariant = (variant: TVariant) => {
7
- const theme = useTheme()
8
-
6
+ export const AlertVariant = (variant: TVariant, theme: DefaultTheme) => {
9
7
  const variants = {
10
8
  success: `
11
9
  border-left: 8px solid ${parseColor(theme.colors.success100)};
@@ -1,15 +1,14 @@
1
1
  import { ButtonVariantTypes } from './Button.types'
2
2
  import { colorType } from '../../types'
3
- import { useTheme } from 'styled-components'
3
+ import { DefaultTheme } from 'styled-components'
4
4
  import { darkenOrLighten, getTextColor, parseColor, getHoverColor } from '../../utils'
5
5
 
6
6
 
7
7
  export const ButtonVariant = (
8
8
  color: colorType,
9
- variant: ButtonVariantTypes
9
+ variant: ButtonVariantTypes,
10
+ theme: DefaultTheme
10
11
  ) => {
11
- const theme = useTheme();
12
-
13
12
  const colorValue = theme.colors[color];
14
13
 
15
14
  if (!colorValue) {
@@ -11,7 +11,6 @@ export interface StyledButtonProps extends ButtonProps {
11
11
  export const StyledButton = styled.button<StyledButtonProps>`
12
12
  position: relative;
13
13
  display: flex;
14
- outline: none !important;
15
14
  justify-content: center;
16
15
  align-items: center;
17
16
  width: ${({ $fullWidth }) => ($fullWidth ? '100%' : 'auto')};
@@ -1,4 +1,5 @@
1
1
  import { useState } from 'react'
2
+ import { useTheme } from 'styled-components'
2
3
  import { StyledButton, RippleContainer } from './Button.styles'
3
4
  import { RippleInterface, ButtonProps } from './Button.types'
4
5
  import { ButtonSizes } from './Button.sizes'
@@ -16,14 +17,11 @@ const Button: React.FC<ButtonProps> = ({
16
17
  type = 'button',
17
18
  ...rest
18
19
  }) => {
20
+ const theme = useTheme()
19
21
  const [ripples, setRipples] = useState<RippleInterface[]>([])
20
22
 
21
23
  const buttonSize = ButtonSizes(size)
22
- const buttonVariant = ButtonVariant(color, variant)
23
-
24
- const removeRipple = (ripples: RippleInterface[], rippleId: string) => {
25
- return ripples.filter(r => r.id !== rippleId)
26
- }
24
+ const buttonVariant = ButtonVariant(color, variant, theme)
27
25
 
28
26
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
29
27
  const rect = e.currentTarget.getBoundingClientRect()
@@ -37,7 +35,6 @@ const Button: React.FC<ButtonProps> = ({
37
35
  }
38
36
  setRipples(prev => [...prev, newRipple])
39
37
 
40
- // Chama o onClick fornecido
41
38
  if (onClick) {
42
39
  onClick(e)
43
40
  }
@@ -65,7 +62,7 @@ const Button: React.FC<ButtonProps> = ({
65
62
  height: 20
66
63
  }}
67
64
  onAnimationEnd={() =>
68
- setRipples(prev => removeRipple(prev, ripple.id))
65
+ setRipples(prev => prev.filter(r => r.id !== ripple.id))
69
66
  }
70
67
  />
71
68
  ))}
@@ -3,7 +3,7 @@ import { colorType } from '../../types'
3
3
  export type ButtonSizeTypes = 'sm' | 'md' | 'lg'
4
4
  export type ButtonVariantTypes = 'solid' | 'outline' | 'ghost' | 'neutralOutline' | 'neutralGhost'
5
5
 
6
- export interface ButtonProps {
6
+ export interface ButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'color' | 'onClick'> {
7
7
  variant?: ButtonVariantTypes
8
8
  children: React.ReactNode
9
9
  className?: string
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react'
1
+ import React, { useEffect, useState } from 'react'
2
2
  import { CheckboxProps } from './Checkbox.types'
3
3
  import {
4
4
  CheckboxWrapper,
@@ -31,9 +31,9 @@ const Checkbox: React.FC<CheckboxProps> = ({
31
31
  )
32
32
  const [ripples, setRipples] = useState<RippleInterface[]>([])
33
33
 
34
- const removeRipple = (ripples: RippleInterface[], rippleId: string) => {
35
- return ripples.filter(r => r.id !== rippleId)
36
- }
34
+ useEffect(() => {
35
+ if (checked !== undefined) setInternalChecked(checked)
36
+ }, [checked])
37
37
 
38
38
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
39
39
  onChange?.(e)
@@ -78,7 +78,7 @@ const Checkbox: React.FC<CheckboxProps> = ({
78
78
  height: 20
79
79
  }}
80
80
  onAnimationEnd={() =>
81
- setRipples(prev => removeRipple(prev, ripple.id))
81
+ setRipples(prev => prev.filter(r => r.id !== ripple.id))
82
82
  }
83
83
  />
84
84
  ))}
@@ -80,7 +80,7 @@ export const PopoverContent = styled(Popover.Content)<{
80
80
  $width: number | string
81
81
  }>`
82
82
  width: ${({ $width }) =>
83
- typeof $width === 'number' ? `${$width}px` : $width};
83
+ typeof $width === 'number' ? `${$width}px` : $width === '100%' ? 'var(--radix-popover-trigger-width)' : $width};
84
84
  background: ${({ theme }) => parseColor(theme.colors.white)};
85
85
  border: 1px solid ${({ theme }) => parseColor(theme.colors.neutral400)};
86
86
  border-radius: 4px;
@@ -33,7 +33,7 @@ const Combobox = (props: ComboboxProps) => {
33
33
  searchPlaceholder = 'Pesquisar',
34
34
  emptyText = 'Sem resultados.',
35
35
  disabled = false,
36
- width = 280,
36
+ width = '100%',
37
37
  size = 'md',
38
38
  onCreate,
39
39
  createLabel = 'Adicionar novo'
@@ -9,6 +9,7 @@ export const Overlay = styled(DialogPrimitive.Overlay)`
9
9
  background: rgba(0, 0, 0, 0.3);
10
10
  position: fixed;
11
11
  inset: 0;
12
+ z-index: 9999;
12
13
  `
13
14
 
14
15
  export const Content = styled(DialogPrimitive.Content)<{$centerContent?: boolean}>`
@@ -27,7 +28,7 @@ export const Content = styled(DialogPrimitive.Content)<{$centerContent?: boolean
27
28
  top: 8vh;
28
29
  left: 50%;
29
30
  transform: translateX(-50%);
30
- z-index: 99999;
31
+ z-index: 10000;
31
32
  `
32
33
 
33
34
  export const TitleDescriptionContainer = styled.div<{$centerContent?: boolean, $variant?: variant}>`
@@ -40,7 +40,7 @@ export const Dialog: React.FC<TDialogProps> = ({
40
40
  onOpenChange={onOpenChange}
41
41
  >
42
42
  {trigger
43
- && <DialogPrimitive.Trigger asChild className={className} >
43
+ && <DialogPrimitive.Trigger asChild>
44
44
  {trigger}
45
45
  </DialogPrimitive.Trigger>
46
46
  }
@@ -85,15 +85,13 @@ export const Dialog: React.FC<TDialogProps> = ({
85
85
  )}
86
86
 
87
87
  <ButtonContainer $centerContent={centerContent}>
88
- <DialogPrimitive.Close asChild>
89
- {cancelButton && (
90
- <Button
91
- variant="neutralOutline"
92
- >
88
+ {cancelButton && (
89
+ <DialogPrimitive.Close asChild>
90
+ <Button variant="neutralOutline">
93
91
  {cancelLabel}
94
92
  </Button>
95
- )}
96
- </DialogPrimitive.Close>
93
+ </DialogPrimitive.Close>
94
+ )}
97
95
 
98
96
  {confirmButton && (
99
97
  <Button
@@ -1,4 +1,5 @@
1
1
  import { useState } from 'react'
2
+ import { useTheme } from 'styled-components'
2
3
  import { ButtonProps, RippleInterface } from '../Button/Button.types'
3
4
  import { ButtonVariant } from '../Button/Button.appearance'
4
5
  import { IconButtonSizes } from './IconButton.sizes'
@@ -15,10 +16,11 @@ const IconButton: React.FC<ButtonProps> = ({
15
16
  onClick,
16
17
  ...rest
17
18
  }) => {
19
+ const theme = useTheme()
18
20
  const [ripples, setRipples] = useState<RippleInterface[]>([])
19
21
 
20
22
  const buttonSize = IconButtonSizes(size)
21
- const buttonVariant = ButtonVariant(color, variant)
23
+ const buttonVariant = ButtonVariant(color, variant, theme)
22
24
 
23
25
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
24
26
  const rect = e.currentTarget.getBoundingClientRect()
@@ -1,4 +1,5 @@
1
1
  import React, { useState } from 'react'
2
+ import { useTheme } from 'styled-components'
2
3
  import { LinkAnchor, StyledLinkButton } from './LinkButton.style'
3
4
  import { RippleContainer } from '../Button/Button.styles'
4
5
  import { RippleInterface } from '../Button/Button.types'
@@ -19,8 +20,9 @@ const LinkButton: React.FC<LinkButtonProps> = ({
19
20
  size = 'md',
20
21
  ...rest
21
22
  }) => {
23
+ const theme = useTheme()
22
24
  const [ripples, setRipples] = useState<RippleInterface[]>([])
23
- const buttonVariant = ButtonVariant(color, variant)
25
+ const buttonVariant = ButtonVariant(color, variant, theme)
24
26
  const buttonSize = LinkButtonSizes(size)
25
27
 
26
28
  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
@@ -16,7 +16,7 @@ const PageWrapper = styled.div.withConfig({
16
16
  })<WrapperProps>`
17
17
  width: 100%;
18
18
  height: 100%;
19
- max-width: ${({width}) => width + 'px' || '100%'};
19
+ max-width: ${({width}) => width ? width + 'px' : '100%'};
20
20
  margin: 0 auto;
21
21
  padding: ${({padding}) =>
22
22
  padding ? spacing[padding] + 'px' : spacing.spacing16 + 'px'};
@@ -0,0 +1,203 @@
1
+ import { useState } from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import { ArticleNyTimesIcon, ChatCircleIcon, EnvelopeSimpleIcon } from '@phosphor-icons/react'
4
+ import RadioCardGroup from './RadioCardGroup'
5
+
6
+ const LONG_DESC = 'Ideal para comunicações diretas com alta taxa de entrega. Suporta texto simples, links e variáveis personalizadas. Amplamente compatível com todos os dispositivos móveis, sem necessidade de internet ou aplicativo instalado.'
7
+
8
+ const meta: Meta<typeof RadioCardGroup> = {
9
+ title: 'Form/RadioCardGroup',
10
+ component: RadioCardGroup,
11
+ parameters: { layout: 'centered' },
12
+ tags: ['autodocs'],
13
+ }
14
+
15
+ export default meta
16
+ type Story = StoryObj<typeof meta>
17
+
18
+ export const Default: Story = {
19
+ render: () => {
20
+ const [selected, setSelected] = useState('sms')
21
+ return (
22
+ <div style={{ width: 320 }}>
23
+ <RadioCardGroup
24
+ name="channel"
25
+ value={selected}
26
+ onChange={setSelected}
27
+ options={[
28
+ {
29
+ value: 'sms',
30
+ label: 'SMS',
31
+ icon: <ChatCircleIcon size={20} />,
32
+ description: LONG_DESC,
33
+ descriptionMaxLines: 2,
34
+ badgesLabel: 'O que contém:',
35
+ badges: ['Texto até 1.600 caracteres'],
36
+ },
37
+ {
38
+ value: 'email',
39
+ label: 'E-mail',
40
+ icon: <EnvelopeSimpleIcon size={20} />,
41
+ description: 'Envio de mensagens por e-mail.',
42
+ badgesLabel: 'O que contém:',
43
+ badges: ['Texto', 'Imagens', 'Links'],
44
+ },
45
+ {
46
+ value: 'rcs',
47
+ label: 'RCS',
48
+ icon: <ArticleNyTimesIcon size={20} />,
49
+ description: 'Mensagens ricas com mídia.',
50
+ badgesLabel: 'O que contém:',
51
+ badges: ['Texto', 'Imagens', 'Botões', 'Carrossel'],
52
+ },
53
+ ]}
54
+ />
55
+ </div>
56
+ )
57
+ },
58
+ }
59
+
60
+ export const WithSeeMore: Story = {
61
+ name: 'Ver mais (descrição longa)',
62
+ render: () => {
63
+ const [selected, setSelected] = useState('a')
64
+ return (
65
+ <div style={{ width: 320 }}>
66
+ <RadioCardGroup
67
+ name="see-more"
68
+ value={selected}
69
+ onChange={setSelected}
70
+ options={[
71
+ {
72
+ value: 'a',
73
+ label: 'Canal A',
74
+ icon: <ChatCircleIcon size={20} />,
75
+ description: LONG_DESC,
76
+ descriptionMaxLines: 2,
77
+ badges: ['Feature 1', 'Feature 2'],
78
+ },
79
+ {
80
+ value: 'b',
81
+ label: 'Canal B',
82
+ icon: <EnvelopeSimpleIcon size={20} />,
83
+ description: 'Descrição curta, sem botão de ver mais.',
84
+ badges: ['Feature 3'],
85
+ },
86
+ ]}
87
+ />
88
+ </div>
89
+ )
90
+ },
91
+ }
92
+
93
+ export const Scrollable: Story = {
94
+ name: 'Com scroll thin',
95
+ render: () => {
96
+ const [selected, setSelected] = useState('a')
97
+ return (
98
+ <div style={{ width: 320 }}>
99
+ <RadioCardGroup
100
+ name="scroll"
101
+ value={selected}
102
+ onChange={setSelected}
103
+ scrollable
104
+ maxHeight="300px"
105
+ options={Array.from({ length: 6 }, (_, i) => ({
106
+ value: String(i),
107
+ label: `Opção ${i + 1}`,
108
+ icon: <ChatCircleIcon size={20} />,
109
+ description: `Descrição da opção ${i + 1}.`,
110
+ badges: [`Badge ${i + 1}`],
111
+ }))}
112
+ />
113
+ </div>
114
+ )
115
+ },
116
+ }
117
+
118
+ export const TwoColumns: Story = {
119
+ name: '2 colunas',
120
+ render: () => {
121
+ const [selected, setSelected] = useState('a')
122
+ return (
123
+ <div style={{ width: 600 }}>
124
+ <RadioCardGroup
125
+ name="grid"
126
+ value={selected}
127
+ onChange={setSelected}
128
+ columns={2}
129
+ options={[
130
+ { value: 'a', label: 'SMS', icon: <ChatCircleIcon size={20} />, description: 'Texto simples.', badges: ['160 chars'] },
131
+ { value: 'b', label: 'E-mail', icon: <EnvelopeSimpleIcon size={20} />, description: 'HTML completo.', badges: ['Ilimitado'] },
132
+ { value: 'c', label: 'RCS', icon: <ArticleNyTimesIcon size={20} />, description: 'Mídia rica.', badges: ['Botões', 'Carrossel'] },
133
+ { value: 'd', label: 'Push', icon: <ChatCircleIcon size={20} />, description: 'Notificação push.', badges: ['Deep link'] },
134
+ ]}
135
+ />
136
+ </div>
137
+ )
138
+ },
139
+ }
140
+
141
+ export const WithoutIcon: Story = {
142
+ render: () => {
143
+ const [selected, setSelected] = useState('a')
144
+ return (
145
+ <div style={{ width: 320 }}>
146
+ <RadioCardGroup
147
+ name="plan"
148
+ value={selected}
149
+ onChange={setSelected}
150
+ options={[
151
+ { value: 'a', label: 'Plano Básico', description: 'Funcionalidades essenciais.', badgesLabel: 'Inclui:', badges: ['5 usuários', '10GB'] },
152
+ { value: 'b', label: 'Plano Pro', description: 'Para times maiores.', badgesLabel: 'Inclui:', badges: ['Ilimitado', '100GB', 'Suporte'] },
153
+ ]}
154
+ />
155
+ </div>
156
+ )
157
+ },
158
+ }
159
+
160
+ export const Disabled: Story = {
161
+ render: () => (
162
+ <div style={{ width: 320 }}>
163
+ <RadioCardGroup
164
+ name="disabled"
165
+ value="a"
166
+ disabled
167
+ options={[
168
+ {
169
+ value: 'a',
170
+ label: 'Opção desabilitada',
171
+ icon: <ChatCircleIcon size={20} />,
172
+ description: 'Este grupo está desabilitado.',
173
+ badges: ['Indisponível'],
174
+ },
175
+ ]}
176
+ />
177
+ </div>
178
+ ),
179
+ }
180
+
181
+ export const CardWidth: Story = {
182
+ name: 'Largura dos cards (min/max)',
183
+ render: () => {
184
+ const [selected, setSelected] = useState('a')
185
+ return (
186
+ <div style={{ width: 700 }}>
187
+ <RadioCardGroup
188
+ name="width"
189
+ value={selected}
190
+ onChange={setSelected}
191
+ columns={3}
192
+ minCardWidth="160px"
193
+ maxCardWidth="240px"
194
+ options={[
195
+ { value: 'a', label: 'SMS', icon: <ChatCircleIcon size={20} />, description: 'Texto simples.', badges: ['160 chars'] },
196
+ { value: 'b', label: 'E-mail', icon: <EnvelopeSimpleIcon size={20} />, description: 'HTML completo.', badges: ['Ilimitado'] },
197
+ { value: 'c', label: 'RCS', icon: <ArticleNyTimesIcon size={20} />, description: 'Mídia rica.', badges: ['Botões'] },
198
+ ]}
199
+ />
200
+ </div>
201
+ )
202
+ },
203
+ }
@@ -0,0 +1,198 @@
1
+ import styled from 'styled-components'
2
+ import { fontSize, fontWeight, lineHeight, radius, spacing } from '@liguelead/foundation'
3
+ import { parseColor } from '../../utils'
4
+
5
+ export const CardWrapper = styled.label<{
6
+ $checked?: boolean
7
+ $disabled?: boolean
8
+ $error?: boolean
9
+ $columns?: number
10
+ $minCardWidth?: string
11
+ $maxCardWidth?: string
12
+ }>`
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: ${spacing.spacing16}px;
16
+ padding: ${spacing.spacing24}px;
17
+ border-radius: ${radius.radius8}px;
18
+ border: 1px solid ${({ theme, $checked, $error }) =>
19
+ $error
20
+ ? parseColor(theme.colors.danger200)
21
+ : $checked
22
+ ? parseColor(theme.colors.primary)
23
+ : parseColor(theme.colors.neutral400)};
24
+ background: ${({ theme, $checked, $error }) =>
25
+ $checked && !$error
26
+ ? parseColor(theme.colors.primaryLight)
27
+ : parseColor(theme.colors.white)};
28
+ cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
29
+ opacity: ${({ $disabled }) => ($disabled ? 0.5 : 1)};
30
+ transition: border-color 0.2s ease, background 0.2s ease;
31
+ flex: ${({ $columns }) => ($columns ? `1 1 calc(${100 / $columns}% - ${spacing.spacing12}px)` : '1 1 100%')};
32
+ min-width: ${({ $minCardWidth }) => $minCardWidth ?? '0'};
33
+ max-width: ${({ $maxCardWidth }) => $maxCardWidth ?? 'none'};
34
+
35
+ &:hover:not([data-disabled='true']) {
36
+ border-color: ${({ theme, $error }) =>
37
+ $error
38
+ ? parseColor(theme.colors.danger200)
39
+ : parseColor(theme.colors.primary)};
40
+ }
41
+ `
42
+
43
+ export const CardRadioRow = styled.div`
44
+ display: flex;
45
+ align-items: flex-start;
46
+ gap: ${spacing.spacing12}px;
47
+ `
48
+
49
+ export const RadioCircle = styled.input`
50
+ appearance: none;
51
+ width: ${spacing.spacing16}px;
52
+ height: ${spacing.spacing16}px;
53
+ min-width: ${spacing.spacing16}px;
54
+ border: 1px solid ${({ theme }) => parseColor(theme.colors.neutral400)};
55
+ border-radius: 50%;
56
+ background: ${({ theme }) => parseColor(theme.colors.white)};
57
+ position: relative;
58
+ cursor: pointer;
59
+ transition: all 0.2s ease;
60
+ margin-top: 2px;
61
+
62
+ &:checked {
63
+ border-color: ${({ theme }) => parseColor(theme.colors.neutral300)};
64
+
65
+ &::after {
66
+ content: '';
67
+ position: absolute;
68
+ top: 50%;
69
+ left: 50%;
70
+ transform: translate(-50%, -50%);
71
+ width: 8px;
72
+ height: 8px;
73
+ border-radius: 50%;
74
+ background: ${({ theme }) => parseColor(theme.colors.primary)};
75
+ }
76
+ }
77
+
78
+ &:disabled {
79
+ cursor: not-allowed;
80
+ border-color: ${({ theme }) => parseColor(theme.colors.neutral400)};
81
+ background: ${({ theme }) => parseColor(theme.colors.neutral100)};
82
+ }
83
+ `
84
+
85
+ export const CardTitleRow = styled.div`
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 6px;
89
+ `
90
+
91
+ export const CardIconWrapper = styled.div`
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ width: 20px;
96
+ height: 20px;
97
+ flex-shrink: 0;
98
+
99
+ svg {
100
+ width: 20px;
101
+ height: 20px;
102
+ }
103
+ `
104
+
105
+ export const Divider = styled.div`
106
+ width: 100%;
107
+ height: 1px;
108
+ background: ${({ theme }) => parseColor(theme.colors.neutral300)};
109
+ `
110
+
111
+ export const CardContent = styled.div`
112
+ display: flex;
113
+ flex-direction: column;
114
+ gap: ${spacing.spacing8}px;
115
+ `
116
+
117
+ export const DescriptionWrapper = styled.div<{ $expanded: boolean; $maxLines: number }>`
118
+ position: relative;
119
+ overflow: hidden;
120
+ max-height: ${({ $expanded, $maxLines }) =>
121
+ $expanded ? 'none' : `calc(${lineHeight.lineHeight16}px * ${$maxLines})`};
122
+ `
123
+
124
+ export const DescriptionText = styled.p`
125
+ font-size: ${fontSize.fontSize12}px;
126
+ font-weight: ${fontWeight.fontWeight400};
127
+ line-height: ${lineHeight.lineHeight16}px;
128
+ color: ${({ theme }) => parseColor(theme.colors.textDark)};
129
+ margin: 0;
130
+ `
131
+
132
+ export const SeeMoreButton = styled.button`
133
+ background: none;
134
+ border: none;
135
+ padding: 0;
136
+ cursor: pointer;
137
+ font-size: ${fontSize.fontSize12}px;
138
+ font-weight: ${fontWeight.fontWeight500};
139
+ line-height: ${lineHeight.lineHeight16}px;
140
+ color: ${({ theme }) => parseColor(theme.colors.primary)};
141
+ text-align: left;
142
+ margin-top: 2px;
143
+
144
+ &:hover {
145
+ text-decoration: underline;
146
+ }
147
+ `
148
+
149
+ export const BadgesSection = styled.div`
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: ${spacing.spacing8}px;
153
+ `
154
+
155
+ export const BadgesLabel = styled.p`
156
+ font-size: ${fontSize.fontSize12}px;
157
+ font-weight: ${fontWeight.fontWeight500};
158
+ line-height: ${lineHeight.lineHeight16}px;
159
+ color: ${({ theme }) => parseColor(theme.colors.textMedium)};
160
+ margin: 0;
161
+ white-space: nowrap;
162
+ `
163
+
164
+ export const BadgesList = styled.div`
165
+ display: flex;
166
+ flex-wrap: wrap;
167
+ gap: ${spacing.spacing4}px;
168
+ align-items: center;
169
+ `
170
+
171
+ export const GroupWrapper = styled.div<{ $columns?: number; $minCardWidth?: string; $scrollable?: boolean; $maxHeight?: string }>`
172
+ display: flex;
173
+ flex-wrap: wrap;
174
+ flex-direction: ${({ $columns }) => ($columns ? 'row' : 'column')};
175
+ gap: ${spacing.spacing12}px;
176
+ width: 100%;
177
+
178
+ ${({ $scrollable, $maxHeight }) => $scrollable && `
179
+ overflow-y: auto;
180
+ max-height: ${$maxHeight ?? '400px'};
181
+ padding-right: ${spacing.spacing4}px;
182
+
183
+ scrollbar-width: thin;
184
+ scrollbar-color: #7c7c83 #cfcfd1;
185
+
186
+ &::-webkit-scrollbar {
187
+ width: 8px;
188
+ }
189
+ &::-webkit-scrollbar-track {
190
+ background: #cfcfd1;
191
+ border-radius: 100px;
192
+ }
193
+ &::-webkit-scrollbar-thumb {
194
+ background: #7c7c83;
195
+ border-radius: 100px;
196
+ }
197
+ `}
198
+ `