@12-apps/ui 1.4.0 → 1.4.1

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 (53) hide show
  1. package/package.json +4 -4
  2. package/src/components/data-display/Carousel/Carousel.chrome.tsx +83 -0
  3. package/src/components/data-display/Carousel/Carousel.hooks.ts +88 -0
  4. package/src/components/data-display/Carousel/Carousel.parts.tsx +346 -0
  5. package/src/components/data-display/Carousel/Carousel.slide.tsx +190 -0
  6. package/src/components/data-display/Carousel/Carousel.styles.ts +86 -0
  7. package/src/components/data-display/Carousel/Carousel.tsx +84 -732
  8. package/src/components/data-display/HoverCard/HoverCard.animations.ts +73 -0
  9. package/src/components/data-display/HoverCard/HoverCard.content.tsx +143 -0
  10. package/src/components/data-display/HoverCard/HoverCard.hooks.ts +180 -0
  11. package/src/components/data-display/HoverCard/HoverCard.styles.ts +178 -0
  12. package/src/components/data-display/HoverCard/HoverCard.tsx +117 -631
  13. package/src/components/data-display/Sheet/Sheet.animations.ts +44 -0
  14. package/src/components/data-display/Sheet/Sheet.body.tsx +124 -0
  15. package/src/components/data-display/Sheet/Sheet.drag.ts +203 -0
  16. package/src/components/data-display/Sheet/Sheet.hooks.ts +318 -0
  17. package/src/components/data-display/Sheet/Sheet.parts.tsx +224 -0
  18. package/src/components/data-display/Sheet/Sheet.styles.ts +257 -0
  19. package/src/components/data-display/Sheet/Sheet.tsx +125 -948
  20. package/src/components/feedback/Modal/Modal.styles.ts +126 -0
  21. package/src/components/feedback/Modal/Modal.transitions.tsx +40 -0
  22. package/src/components/feedback/Modal/Modal.tsx +37 -242
  23. package/src/components/form/InputOTP/InputOTP.hooks.ts +104 -0
  24. package/src/components/form/InputOTP/InputOTP.styles.ts +97 -0
  25. package/src/components/form/InputOTP/InputOTP.tsx +74 -221
  26. package/src/components/form/Label/Label.parts.tsx +75 -0
  27. package/src/components/form/Label/Label.styles.ts +206 -0
  28. package/src/components/form/Label/Label.tsx +73 -325
  29. package/src/components/form/Menubar/Menubar.hooks.ts +41 -0
  30. package/src/components/form/Menubar/Menubar.item.tsx +121 -0
  31. package/src/components/form/Menubar/Menubar.separator.tsx +25 -0
  32. package/src/components/form/Menubar/Menubar.styles.ts +110 -0
  33. package/src/components/form/Menubar/Menubar.toolbar.tsx +103 -0
  34. package/src/components/form/Menubar/Menubar.tsx +64 -347
  35. package/src/components/form/RadioGroup/RadioGroup.animations.ts +48 -0
  36. package/src/components/form/RadioGroup/RadioGroup.styles.ts +305 -0
  37. package/src/components/form/RadioGroup/RadioGroup.tsx +81 -651
  38. package/src/components/form/RadioGroup/RadioGroup.variants.tsx +291 -0
  39. package/src/components/form/Slider/Slider.animations.ts +40 -0
  40. package/src/components/form/Slider/Slider.styles.ts +292 -0
  41. package/src/components/form/Slider/Slider.tsx +106 -357
  42. package/src/components/form/Switch/Switch.animations.ts +62 -0
  43. package/src/components/form/Switch/Switch.parts.tsx +213 -0
  44. package/src/components/form/Switch/Switch.styles.ts +355 -0
  45. package/src/components/form/Switch/Switch.tsx +87 -566
  46. package/src/components/typography/Blockquote/Blockquote.test.stories.tsx +7 -7
  47. package/src/components/typography/Heading/Heading.styles.ts +101 -0
  48. package/src/components/typography/Heading/Heading.tsx +48 -164
  49. package/src/components/utility/AnimatedIcon/AnimatedIcon.animations.ts +274 -0
  50. package/src/components/utility/AnimatedIcon/AnimatedIcon.styles.ts +231 -0
  51. package/src/components/utility/AnimatedIcon/AnimatedIcon.tsx +39 -559
  52. package/src/utils/index.ts +1 -0
  53. 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
+ `;