@12-apps/ui 1.4.0 → 1.5.0

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 (66) hide show
  1. package/eslint.config.js +12 -0
  2. package/package.json +5 -4
  3. package/src/components/data-display/AlertDialog/AlertDialog.test.stories.tsx +16 -6
  4. package/src/components/data-display/Banner/Banner.test.stories.tsx +26 -18
  5. package/src/components/data-display/Carousel/Carousel.chrome.tsx +83 -0
  6. package/src/components/data-display/Carousel/Carousel.hooks.ts +88 -0
  7. package/src/components/data-display/Carousel/Carousel.parts.tsx +346 -0
  8. package/src/components/data-display/Carousel/Carousel.slide.tsx +190 -0
  9. package/src/components/data-display/Carousel/Carousel.styles.ts +86 -0
  10. package/src/components/data-display/Carousel/Carousel.test.stories.tsx +15 -5
  11. package/src/components/data-display/Carousel/Carousel.tsx +84 -732
  12. package/src/components/data-display/HoverCard/HoverCard.animations.ts +73 -0
  13. package/src/components/data-display/HoverCard/HoverCard.content.tsx +143 -0
  14. package/src/components/data-display/HoverCard/HoverCard.hooks.ts +180 -0
  15. package/src/components/data-display/HoverCard/HoverCard.styles.ts +178 -0
  16. package/src/components/data-display/HoverCard/HoverCard.tsx +117 -631
  17. package/src/components/data-display/Sheet/Sheet.animations.ts +44 -0
  18. package/src/components/data-display/Sheet/Sheet.body.tsx +124 -0
  19. package/src/components/data-display/Sheet/Sheet.drag.ts +203 -0
  20. package/src/components/data-display/Sheet/Sheet.hooks.ts +318 -0
  21. package/src/components/data-display/Sheet/Sheet.parts.tsx +224 -0
  22. package/src/components/data-display/Sheet/Sheet.styles.ts +257 -0
  23. package/src/components/data-display/Sheet/Sheet.test.stories.tsx +25 -21
  24. package/src/components/data-display/Sheet/Sheet.tsx +125 -948
  25. package/src/components/feedback/Dialog/Dialog.test.stories.tsx +37 -28
  26. package/src/components/feedback/Modal/Modal.styles.ts +126 -0
  27. package/src/components/feedback/Modal/Modal.test.stories.tsx +5 -5
  28. package/src/components/feedback/Modal/Modal.transitions.tsx +40 -0
  29. package/src/components/feedback/Modal/Modal.tsx +37 -242
  30. package/src/components/feedback/StackedModal/StackedModal.test.stories.tsx +9 -11
  31. package/src/components/form/InputOTP/InputOTP.hooks.ts +104 -0
  32. package/src/components/form/InputOTP/InputOTP.styles.ts +97 -0
  33. package/src/components/form/InputOTP/InputOTP.tsx +74 -221
  34. package/src/components/form/Label/Label.parts.tsx +75 -0
  35. package/src/components/form/Label/Label.styles.ts +206 -0
  36. package/src/components/form/Label/Label.test.stories.tsx +20 -14
  37. package/src/components/form/Label/Label.tsx +73 -325
  38. package/src/components/form/Menubar/Menubar.hooks.ts +41 -0
  39. package/src/components/form/Menubar/Menubar.item.tsx +121 -0
  40. package/src/components/form/Menubar/Menubar.separator.tsx +25 -0
  41. package/src/components/form/Menubar/Menubar.styles.ts +110 -0
  42. package/src/components/form/Menubar/Menubar.toolbar.tsx +103 -0
  43. package/src/components/form/Menubar/Menubar.tsx +64 -347
  44. package/src/components/form/RadioGroup/RadioGroup.animations.ts +48 -0
  45. package/src/components/form/RadioGroup/RadioGroup.styles.ts +305 -0
  46. package/src/components/form/RadioGroup/RadioGroup.tsx +81 -651
  47. package/src/components/form/RadioGroup/RadioGroup.variants.tsx +291 -0
  48. package/src/components/form/Slider/Slider.animations.ts +40 -0
  49. package/src/components/form/Slider/Slider.styles.ts +292 -0
  50. package/src/components/form/Slider/Slider.tsx +106 -357
  51. package/src/components/form/Switch/Switch.animations.ts +62 -0
  52. package/src/components/form/Switch/Switch.parts.tsx +213 -0
  53. package/src/components/form/Switch/Switch.styles.ts +355 -0
  54. package/src/components/form/Switch/Switch.tsx +87 -566
  55. package/src/components/form/ToggleGroup/ToggleGroup.test.stories.tsx +26 -29
  56. package/src/components/layout/Resizable/Resizable.test.stories.tsx +11 -7
  57. package/src/components/layout/Spacer/Spacer.test.stories.tsx +11 -9
  58. package/src/components/typography/Blockquote/Blockquote.test.stories.tsx +7 -7
  59. package/src/components/typography/Heading/Heading.styles.ts +101 -0
  60. package/src/components/typography/Heading/Heading.tsx +48 -164
  61. package/src/components/typography/Text/Text.test.stories.tsx +27 -30
  62. package/src/components/utility/AnimatedIcon/AnimatedIcon.animations.ts +274 -0
  63. package/src/components/utility/AnimatedIcon/AnimatedIcon.styles.ts +231 -0
  64. package/src/components/utility/AnimatedIcon/AnimatedIcon.tsx +39 -559
  65. package/src/utils/index.ts +1 -0
  66. package/src/utils/withDefaults.ts +20 -0
@@ -1,374 +1,123 @@
1
- import type { Theme} from '@mui/material';
2
- import { alpha, Box, keyframes, Slider as MuiSlider,Typography } from '@mui/material';
1
+ import { Box, Slider as MuiSlider, Typography } from '@mui/material';
3
2
  import { styled } from '@mui/material/styles';
4
3
  import React, { forwardRef } from 'react';
5
4
 
6
- import type { SliderProps } from './Slider.types';
7
-
8
- const glowAnimation = keyframes`
9
- 0% {
10
- box-shadow: 0 0 5px currentColor;
11
- }
12
- 50% {
13
- box-shadow: 0 0 15px currentColor, 0 0 25px currentColor;
14
- }
15
- 100% {
16
- box-shadow: 0 0 5px currentColor;
17
- }
18
- `;
19
-
20
- const pulseAnimation = keyframes`
21
- 0% {
22
- transform: scale(1);
23
- opacity: 1;
24
- }
25
- 50% {
26
- transform: scale(1.5);
27
- opacity: 0.5;
28
- }
29
- 100% {
30
- transform: scale(2);
31
- opacity: 0;
32
- }
33
- `;
34
-
35
- const gradientShiftAnimation = keyframes`
36
- 0% {
37
- background-position: 0% 50%;
38
- }
39
- 50% {
40
- background-position: 100% 50%;
41
- }
42
- 100% {
43
- background-position: 0% 50%;
44
- }
45
- `;
46
-
47
- const getColorFromTheme = (theme: Theme, color: string) => {
48
- if (color === 'neutral') {
49
- return {
50
- main: theme.palette.grey[700],
51
- dark: theme.palette.grey[800],
52
- light: theme.palette.grey[500],
53
- contrastText: '#fff',
54
- };
55
- }
56
-
57
- const colorMap = {
58
- primary: theme.palette.primary,
59
- secondary: theme.palette.secondary,
60
- success: theme.palette.success,
61
- warning: theme.palette.warning,
62
- danger: theme.palette.error,
63
- } as const;
5
+ import { withDefaults } from '../../../utils/withDefaults';
64
6
 
65
- const palette = colorMap[color as keyof typeof colorMap] || theme.palette.primary;
66
-
67
- // Ensure palette has required properties
68
- return {
69
- main: palette.main,
70
- dark: palette.dark || palette.main,
71
- light: palette.light || palette.main,
72
- contrastText: palette.contrastText || '#fff',
73
- };
74
- };
7
+ import { sliderSx } from './Slider.styles';
8
+ import type { SliderFlags } from './Slider.styles';
9
+ import type { SliderProps } from './Slider.types';
75
10
 
76
11
  const StyledSlider = styled(MuiSlider, {
77
12
  shouldForwardProp: (prop) =>
78
13
  !['customColor', 'customSize', 'glow', 'glass', 'gradient', 'customVariant'].includes(
79
14
  prop as string,
80
15
  ),
81
- })<{
82
- customColor?: string;
83
- customSize?: string;
84
- glow?: boolean;
85
- glass?: boolean;
86
- gradient?: boolean;
87
- customVariant?: string;
88
- }>(({
89
- theme,
90
- customColor = 'primary',
91
- customSize = 'md',
92
- glow,
93
- glass,
94
- gradient,
95
- customVariant,
96
- }) => {
97
- const colorPalette = getColorFromTheme(theme, customColor);
98
-
99
- const sizeMap = {
100
- xs: { height: 4, thumbSize: 16, markHeight: 8 },
101
- sm: { height: 6, thumbSize: 18, markHeight: 10 },
102
- md: { height: 8, thumbSize: 20, markHeight: 12 },
103
- lg: { height: 10, thumbSize: 24, markHeight: 14 },
104
- xl: { height: 12, thumbSize: 28, markHeight: 16 },
105
- };
106
-
107
- const currentSize = sizeMap[customSize as keyof typeof sizeMap];
108
-
109
- return {
110
- color: colorPalette.main,
111
- height: currentSize.height,
112
- '& .MuiSlider-track': {
113
- border: 'none',
114
- height: currentSize.height,
115
- borderRadius: currentSize.height / 2,
116
- transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
117
- position: 'relative',
118
- overflow: 'hidden',
119
- ...(gradient && {
120
- background:
121
- customVariant === 'gradient'
122
- ? `linear-gradient(90deg,
123
- ${colorPalette.light} 0%,
124
- ${colorPalette.main} 50%,
125
- ${colorPalette.dark} 100%)`
126
- : `linear-gradient(90deg, ${colorPalette.light}, ${colorPalette.main})`,
127
- backgroundSize: '200% 100%',
128
- animation:
129
- customVariant === 'gradient' ? `${gradientShiftAnimation} 3s ease infinite` : 'none',
130
- }),
131
- ...(glow && {
132
- animation: `${glowAnimation} 2s ease-in-out infinite`,
133
- boxShadow: `0 0 10px ${alpha(colorPalette.main, 0.6)}, inset 0 0 10px ${alpha(colorPalette.main, 0.2)}`,
134
- }),
135
- ...(glass && {
136
- backgroundColor: alpha(colorPalette.main, 0.8),
137
- backdropFilter: 'blur(10px)',
138
- border: `1px solid ${alpha(colorPalette.light || colorPalette.main, 0.3)}`,
139
- }),
140
- '&::after': gradient
141
- ? {
142
- content: '""',
143
- position: 'absolute',
144
- top: 0,
145
- left: 0,
146
- right: 0,
147
- bottom: 0,
148
- background: `linear-gradient(90deg, transparent, ${alpha('#fff', 0.2)}, transparent)`,
149
- animation: `${gradientShiftAnimation} 2s linear infinite`,
150
- }
151
- : {},
152
- },
153
- '& .MuiSlider-rail': {
154
- color: alpha(theme.palette.action.disabled, 0.3),
155
- opacity: 1,
156
- height: currentSize.height,
157
- borderRadius: currentSize.height / 2,
158
- transition: 'all 0.3s ease',
159
- ...(glass && {
160
- backgroundColor: alpha(theme.palette.background.paper, 0.1),
161
- backdropFilter: 'blur(20px)',
162
- border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
163
- }),
164
- ...(customVariant === 'gradient' && {
165
- background: `linear-gradient(90deg,
166
- ${alpha(theme.palette.action.disabled, 0.2)},
167
- ${alpha(theme.palette.action.disabled, 0.3)},
168
- ${alpha(theme.palette.action.disabled, 0.2)})`,
169
- }),
170
- },
171
- '& .MuiSlider-thumb': {
172
- height: currentSize.thumbSize,
173
- width: currentSize.thumbSize,
174
- backgroundColor: gradient ? colorPalette.main : '#fff',
175
- border: `2px solid ${colorPalette.main}`,
176
- boxShadow: `${theme.shadows[2]}, 0 0 0 0 ${alpha(colorPalette.main, 0.2)}`,
177
- transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
178
- position: 'relative',
179
- '&::before': {
180
- content: '""',
181
- position: 'absolute',
182
- top: '50%',
183
- left: '50%',
184
- transform: 'translate(-50%, -50%)',
185
- width: '100%',
186
- height: '100%',
187
- borderRadius: '50%',
188
- backgroundColor: colorPalette.main,
189
- opacity: 0,
190
- },
191
- '&:focus, &:hover, &.Mui-active, &.Mui-focusVisible': {
192
- boxShadow: `${theme.shadows[4]}, 0 0 0 8px ${alpha(colorPalette.main, 0.15)}`,
193
- transform: 'scale(1.15)',
194
- ...(glow && {
195
- boxShadow: `${theme.shadows[4]}, 0 0 20px ${alpha(colorPalette.main, 0.6)}, 0 0 0 8px ${alpha(colorPalette.main, 0.15)}`,
196
- }),
197
- '&::before': {
198
- animation: `${pulseAnimation} 0.6s ease-out`,
199
- },
200
- },
201
- '&:active': {
202
- transform: 'scale(1.05)',
203
- },
204
- ...(glass && {
205
- backgroundColor: alpha(theme.palette.background.paper, 0.9),
206
- backdropFilter: 'blur(10px)',
207
- border: `2px solid ${alpha(colorPalette.main, 0.8)}`,
208
- }),
209
- ...(gradient && {
210
- background: `linear-gradient(135deg, ${colorPalette.light}, ${colorPalette.main})`,
211
- border: 'none',
212
- color: '#fff',
213
- }),
214
- },
215
- '& .MuiSlider-valueLabel': {
216
- lineHeight: 1.2,
217
- fontSize: customSize === 'xs' ? 10 : customSize === 'sm' ? 11 : 12,
218
- padding: 0,
219
- width: customSize === 'xs' ? 28 : customSize === 'sm' ? 30 : 32,
220
- height: customSize === 'xs' ? 28 : customSize === 'sm' ? 30 : 32,
221
- borderRadius: gradient ? '8px' : '50% 50% 50% 0',
222
- backgroundColor: gradient
223
- ? 'transparent'
224
- : glass
225
- ? alpha(colorPalette.main, 0.9)
226
- : colorPalette.main,
227
- background: gradient
228
- ? `linear-gradient(135deg, ${colorPalette.light}, ${colorPalette.main})`
229
- : 'unset',
230
- backdropFilter: glass ? 'blur(10px)' : 'none',
231
- transformOrigin: 'bottom left',
232
- transform: gradient
233
- ? 'translate(50%, -150%) scale(0)'
234
- : 'translate(50%, -100%) rotate(-45deg) scale(0)',
235
- transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
236
- boxShadow: `${theme.shadows[2]}, 0 0 10px ${alpha(colorPalette.main, 0.2)}`,
237
- '&:before': { display: 'none' },
238
- '&.MuiSlider-valueLabelOpen': {
239
- transform: gradient
240
- ? 'translate(50%, -150%) scale(1)'
241
- : 'translate(50%, -100%) rotate(-45deg) scale(1)',
242
- },
243
- '& > *': {
244
- transform: gradient ? 'none' : 'rotate(45deg)',
245
- fontWeight: 600,
246
- },
247
- },
248
- '& .MuiSlider-mark': {
249
- backgroundColor: alpha(theme.palette.action.disabled, 0.5),
250
- height: currentSize.markHeight,
251
- width: customVariant === 'marks' ? 3 : 2,
252
- marginTop: -(currentSize.markHeight - currentSize.height) / 2,
253
- borderRadius: 1,
254
- transition: 'all 0.3s ease',
255
- '&.MuiSlider-markActive': {
256
- backgroundColor: gradient ? colorPalette.light : colorPalette.main,
257
- width: customVariant === 'marks' ? 4 : 2,
258
- height: currentSize.markHeight + 2,
259
- marginTop: -(currentSize.markHeight + 2 - currentSize.height) / 2,
260
- ...(glow && {
261
- boxShadow: `0 0 8px ${alpha(colorPalette.main, 0.5)}`,
262
- }),
263
- },
264
- },
265
- '& .MuiSlider-markLabel': {
266
- fontSize: customSize === 'xs' ? '0.65rem' : customSize === 'sm' ? '0.7rem' : '0.75rem',
267
- color: theme.palette.text.secondary,
268
- marginTop: theme.spacing(1.5),
269
- fontWeight: 500,
270
- transition: 'all 0.3s ease',
271
- '&.MuiSlider-markLabelActive': {
272
- color: colorPalette.main,
273
- fontWeight: 600,
274
- transform: 'scale(1.05)',
275
- },
276
- },
277
- };
278
- });
279
-
280
- export const Slider = forwardRef<HTMLSpanElement, SliderProps>(
281
- (
282
- {
283
- variant = 'default',
284
- color = 'primary',
285
- size = 'md',
286
- label,
287
- showValue = false,
288
- glow = false,
289
- glass = false,
290
- gradient = false,
291
- showMarks = false,
292
- customMarks,
293
- unit = '',
294
- formatValue,
295
- value,
296
- onChange,
297
- dataTestId,
298
- ...props
299
- },
300
- ref,
301
- ) => {
302
- const displayValue = Array.isArray(value)
303
- ? `${formatValue ? formatValue(value[0] ?? 0) : (value[0] ?? 0)}${unit} - ${formatValue ? formatValue(value[1] ?? 0) : (value[1] ?? 0)}${unit}`
304
- : `${formatValue ? formatValue((value as number) ?? 0) : (value ?? 0)}${unit}`;
16
+ })<SliderFlags>(({ theme, ...flags }) => ({ ...sliderSx(theme, flags) }));
17
+
18
+ const DEFAULTS = {
19
+ variant: 'default',
20
+ color: 'primary',
21
+ size: 'md',
22
+ showValue: false,
23
+ glow: false,
24
+ glass: false,
25
+ gradient: false,
26
+ showMarks: false,
27
+ unit: '',
28
+ } satisfies Partial<SliderProps>;
29
+
30
+ type ResolvedProps = SliderProps & Required<Pick<SliderProps, keyof typeof DEFAULTS>>;
31
+
32
+ /** One end of the value, formatted by the caller's formatter if it supplied one. */
33
+ const formatOne = (n: number, unit: string, formatValue?: (v: number) => string) =>
34
+ `${formatValue ? formatValue(n) : n}${unit}`;
35
+
36
+ /** A range renders as `low - high`; a single value on its own. */
37
+ const displayValueOf = (
38
+ value: SliderProps['value'],
39
+ unit: string,
40
+ formatValue?: (v: number) => string,
41
+ ) => {
42
+ if (Array.isArray(value)) {
43
+ const low = formatOne(value[0] ?? 0, unit, formatValue);
44
+ const high = formatOne(value[1] ?? 0, unit, formatValue);
45
+ return `${low} - ${high}`;
46
+ }
305
47
 
306
- const marks =
307
- variant === 'marks' || showMarks
308
- ? customMarks || (variant === 'marks' ? true : undefined)
309
- : undefined;
48
+ return formatOne((value as number) ?? 0, unit, formatValue);
49
+ };
310
50
 
311
- return (
312
- <Box sx={{ width: '100%' }} data-testid={dataTestId}>
313
- {(label || showValue) && (
314
- <Box
315
- sx={{
316
- display: 'flex',
317
- justifyContent: 'space-between',
318
- alignItems: 'center',
319
- mb: 2,
320
- }}
321
- >
322
- {label && (
323
- <Typography
324
- variant="body2"
325
- fontWeight={500}
326
- color="text.primary"
327
- data-testid={dataTestId ? `${dataTestId}-label` : undefined}
328
- >
329
- {label}
330
- </Typography>
331
- )}
332
- {showValue && (
333
- <Typography
334
- variant="body2"
335
- color="text.secondary"
336
- data-testid={dataTestId ? `${dataTestId}-value-label` : undefined}
337
- >
338
- {displayValue}
339
- </Typography>
340
- )}
341
- </Box>
342
- )}
51
+ /**
52
+ * Marks come from `customMarks` when given. Otherwise the `marks` variant asks
53
+ * MUI to derive them from the step, and anything else shows none.
54
+ */
55
+ const marksFor = (variant: string, showMarks: boolean, customMarks: SliderProps['customMarks']) => {
56
+ if (variant !== 'marks' && !showMarks) return undefined;
57
+ return customMarks || (variant === 'marks' ? true : undefined);
58
+ };
343
59
 
344
- <Box sx={{ px: 1 }}>
345
- <StyledSlider
346
- ref={ref}
347
- customColor={color}
348
- customSize={size}
349
- glow={glow}
350
- glass={glass}
351
- gradient={gradient}
352
- customVariant={variant}
353
- value={value}
354
- onChange={onChange}
355
- marks={marks}
356
- valueLabelDisplay={showValue ? 'auto' : 'off'}
357
- data-testid={dataTestId ? `${dataTestId}-slider` : undefined}
358
- slotProps={{
359
- track: {
360
- 'data-testid': dataTestId ? `${dataTestId}-track` : undefined,
361
- } as React.HTMLAttributes<HTMLSpanElement>,
362
- thumb: {
363
- 'data-testid': dataTestId ? `${dataTestId}-thumb` : undefined,
364
- } as React.HTMLAttributes<HTMLSpanElement>,
365
- }}
366
- {...props}
367
- />
60
+ export const Slider = forwardRef<HTMLSpanElement, SliderProps>((props, ref) => {
61
+ const {
62
+ variant, color, size, label, showValue, glow, glass, gradient,
63
+ showMarks, customMarks, unit, formatValue, value, onChange, dataTestId,
64
+ ...rest
65
+ } = withDefaults(props, DEFAULTS) as ResolvedProps;
66
+
67
+ const testId = (suffix: string) => (dataTestId ? `${dataTestId}-${suffix}` : undefined);
68
+
69
+ return (
70
+ <Box sx={{ width: '100%' }} data-testid={dataTestId}>
71
+ {(label || showValue) && (
72
+ <Box
73
+ sx={{
74
+ display: 'flex',
75
+ justifyContent: 'space-between',
76
+ alignItems: 'center',
77
+ mb: 2,
78
+ }}
79
+ >
80
+ {label && (
81
+ <Typography
82
+ variant="body2"
83
+ fontWeight={500}
84
+ color="text.primary"
85
+ data-testid={testId('label')}
86
+ >
87
+ {label}
88
+ </Typography>
89
+ )}
90
+ {showValue && (
91
+ <Typography variant="body2" color="text.secondary" data-testid={testId('value-label')}>
92
+ {displayValueOf(value, unit, formatValue)}
93
+ </Typography>
94
+ )}
368
95
  </Box>
96
+ )}
97
+
98
+ <Box sx={{ px: 1 }}>
99
+ <StyledSlider
100
+ ref={ref}
101
+ customColor={color}
102
+ customSize={size}
103
+ glow={glow}
104
+ glass={glass}
105
+ gradient={gradient}
106
+ customVariant={variant}
107
+ value={value}
108
+ onChange={onChange}
109
+ marks={marksFor(variant, showMarks, customMarks)}
110
+ valueLabelDisplay={showValue ? 'auto' : 'off'}
111
+ data-testid={testId('slider')}
112
+ slotProps={{
113
+ track: { 'data-testid': testId('track') } as React.HTMLAttributes<HTMLSpanElement>,
114
+ thumb: { 'data-testid': testId('thumb') } as React.HTMLAttributes<HTMLSpanElement>,
115
+ }}
116
+ {...rest}
117
+ />
369
118
  </Box>
370
- );
371
- },
372
- );
119
+ </Box>
120
+ );
121
+ });
373
122
 
374
123
  Slider.displayName = 'Slider';
@@ -0,0 +1,62 @@
1
+ import { keyframes } from '@mui/material';
2
+
3
+ export const glowAnimation = keyframes`
4
+ 0% {
5
+ box-shadow: 0 0 5px currentColor;
6
+ }
7
+ 50% {
8
+ box-shadow: 0 0 15px currentColor, 0 0 25px currentColor;
9
+ }
10
+ 100% {
11
+ box-shadow: 0 0 5px currentColor;
12
+ }
13
+ `;
14
+
15
+ export const bounceAnimation = keyframes`
16
+ 0%, 100% {
17
+ transform: scale(1);
18
+ }
19
+ 50% {
20
+ transform: scale(1.1);
21
+ }
22
+ `;
23
+
24
+ export const pulseAnimation = keyframes`
25
+ 0%, 100% {
26
+ opacity: 1;
27
+ transform: scale(1);
28
+ }
29
+ 50% {
30
+ opacity: 0.7;
31
+ transform: scale(0.95);
32
+ }
33
+ `;
34
+
35
+ export const rippleAnimation = keyframes`
36
+ 0% {
37
+ transform: scale(0);
38
+ opacity: 1;
39
+ }
40
+ 100% {
41
+ transform: scale(2);
42
+ opacity: 0;
43
+ }
44
+ `;
45
+
46
+ export const spinAnimation = keyframes`
47
+ 0% {
48
+ transform: rotate(0deg);
49
+ }
50
+ 100% {
51
+ transform: rotate(360deg);
52
+ }
53
+ `;
54
+
55
+ export const shimmerAnimation = keyframes`
56
+ 0% {
57
+ background-position: -200% 50%;
58
+ }
59
+ 100% {
60
+ background-position: 200% 50%;
61
+ }
62
+ `;