@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
@@ -0,0 +1,305 @@
1
+ import { alpha } from '@mui/material';
2
+ import type { CSSObject, Theme } from '@mui/material/styles';
3
+
4
+ import { glowAnimation, rippleAnimation, scaleAnimation, slideAnimation } from './RadioGroup.animations';
5
+
6
+ interface ColorPalette {
7
+ main: string;
8
+ dark?: string;
9
+ light?: string;
10
+ contrastText?: string;
11
+ }
12
+
13
+ /**
14
+ * `neutral` is not a MUI palette entry, so it is built from the grey ramp. The
15
+ * literal fallbacks cover a theme whose grey ramp has been replaced with one
16
+ * that omits these steps.
17
+ */
18
+ const neutralPalette = (theme: Theme): ColorPalette => ({
19
+ main: theme.palette.grey?.[700] || '#616161',
20
+ dark: theme.palette.grey?.[800] || '#424242',
21
+ light: theme.palette.grey?.[500] || '#9e9e9e',
22
+ contrastText: '#fff',
23
+ });
24
+
25
+ /** `danger` is this component's name for the error palette; the rest map straight through. */
26
+ const namedPalette = (theme: Theme, color: string): ColorPalette => {
27
+ const colorMap: Record<string, ColorPalette> = {
28
+ primary: theme.palette.primary,
29
+ secondary: theme.palette.secondary,
30
+ success: theme.palette.success,
31
+ warning: theme.palette.warning,
32
+ danger: theme.palette.error,
33
+ };
34
+
35
+ return colorMap[color] || theme.palette.primary;
36
+ };
37
+
38
+ /**
39
+ * Resolves a colour name to a full palette, filling any step the theme leaves
40
+ * out from `main` and then from the primary palette. Split into three so the
41
+ * fallback chains do not all count against one function.
42
+ */
43
+ const getColorFromTheme = (theme: Theme, color: string): ColorPalette => {
44
+ if (color === 'neutral') {
45
+ return neutralPalette(theme);
46
+ }
47
+
48
+ const palette = namedPalette(theme, color);
49
+ const { primary } = theme.palette;
50
+
51
+ // namedPalette always yields a palette, so these need no optional chaining;
52
+ // each step still falls back to `main` and then to primary when the theme
53
+ // leaves it out.
54
+ return {
55
+ main: palette.main || primary.main,
56
+ dark: palette.dark || palette.main || primary.dark,
57
+ light: palette.light || palette.main || primary.light,
58
+ contrastText: palette.contrastText || '#fff',
59
+ };
60
+ };
61
+
62
+ type SizeKey = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
63
+
64
+ const pickSize = <T,>(map: Record<SizeKey, T>, size: string): T => map[size as SizeKey] ?? map.md;
65
+
66
+ const CARD_SIZES: Record<SizeKey, CSSObject> = {
67
+ xs: { padding: '8px', minHeight: '60px' },
68
+ sm: { padding: '12px', minHeight: '70px' },
69
+ md: { padding: '16px', minHeight: '80px' },
70
+ lg: { padding: '20px', minHeight: '90px' },
71
+ xl: { padding: '24px', minHeight: '100px' },
72
+ };
73
+
74
+ const BUTTON_SIZES: Record<SizeKey, CSSObject> = {
75
+ xs: { padding: '6px 12px', fontSize: '0.75rem', minHeight: '32px' },
76
+ sm: { padding: '8px 16px', fontSize: '0.875rem', minHeight: '36px' },
77
+ md: { padding: '10px 20px', fontSize: '1rem', minHeight: '40px' },
78
+ lg: { padding: '12px 24px', fontSize: '1.125rem', minHeight: '44px' },
79
+ xl: { padding: '14px 28px', fontSize: '1.25rem', minHeight: '48px' },
80
+ };
81
+
82
+ const SEGMENT_SIZES: Record<SizeKey, CSSObject> = {
83
+ xs: { padding: '4px 8px', fontSize: '0.75rem' },
84
+ sm: { padding: '6px 12px', fontSize: '0.875rem' },
85
+ md: { padding: '8px 16px', fontSize: '1rem' },
86
+ lg: { padding: '10px 20px', fontSize: '1.125rem' },
87
+ xl: { padding: '12px 24px', fontSize: '1.25rem' },
88
+ };
89
+
90
+ export interface SurfaceFlags {
91
+ selected?: boolean;
92
+ customColor?: string;
93
+ glass?: boolean;
94
+ gradient?: boolean;
95
+ glow?: boolean;
96
+ customSize?: string;
97
+ animated?: boolean;
98
+ }
99
+
100
+ export const formLabelSx = (theme: Theme, glass?: boolean, error?: boolean): CSSObject => ({
101
+ marginBottom: theme.spacing(2),
102
+ fontWeight: 600,
103
+ color: error ? theme.palette.error.main : theme.palette.text.primary,
104
+ ...(glass && {
105
+ backgroundColor: alpha(theme.palette.background.paper, 0.1),
106
+ backdropFilter: 'blur(10px)',
107
+ padding: '8px 12px',
108
+ borderRadius: '8px',
109
+ border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
110
+ display: 'inline-block',
111
+ }),
112
+ });
113
+
114
+ /** The card's resting surface, before glass/gradient/glow override parts of it. */
115
+ const cardBase = (theme: Theme, flags: SurfaceFlags, palette: ColorPalette): CSSObject => {
116
+ const { selected, animated, customSize = 'md' } = flags;
117
+
118
+ return {
119
+ cursor: 'pointer',
120
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
121
+ border: `2px solid ${selected ? palette.main : alpha(theme.palette.divider, 0.3)}`,
122
+ backgroundColor: selected ? alpha(palette.main, 0.05) : theme.palette.background.paper,
123
+ position: 'relative' as const,
124
+ overflow: 'hidden' as const,
125
+ ...(animated && { animation: `${slideAnimation} 0.4s ease-out` }),
126
+ // A radial wash that grows from the centre as the card is selected or hovered.
127
+ '&::before': {
128
+ content: '""',
129
+ position: 'absolute',
130
+ top: '50%',
131
+ left: '50%',
132
+ width: selected ? '100%' : '0',
133
+ height: selected ? '100%' : '0',
134
+ background: `radial-gradient(circle, ${alpha(palette.main, 0.1)} 0%, transparent 70%)`,
135
+ transform: 'translate(-50%, -50%)',
136
+ transition: 'all 0.5s ease',
137
+ borderRadius: '50%',
138
+ },
139
+ '&:hover': {
140
+ borderColor: palette.main,
141
+ backgroundColor: alpha(palette.main, 0.02),
142
+ transform: 'translateY(-2px) scale(1.02)',
143
+ boxShadow: `${theme.shadows[4]}, 0 10px 30px -5px ${alpha(palette.main, 0.2)}`,
144
+ '&::before': { width: '120%', height: '120%' },
145
+ },
146
+ '&:active': { transform: 'scale(0.98)' },
147
+ ...pickSize(CARD_SIZES, customSize),
148
+ };
149
+ };
150
+
151
+ export const radioCardSx = (theme: Theme, flags: SurfaceFlags): CSSObject => {
152
+ const { selected, customColor = 'primary', glass, gradient, glow } = flags;
153
+ const palette = getColorFromTheme(theme, customColor);
154
+
155
+ return {
156
+ ...cardBase(theme, flags, palette),
157
+ ...(glass && {
158
+ backgroundColor: selected
159
+ ? alpha(palette.main, 0.1)
160
+ : alpha(theme.palette.background.paper, 0.1),
161
+ backdropFilter: 'blur(20px)',
162
+ border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
163
+ }),
164
+ ...(gradient &&
165
+ selected && {
166
+ background: `linear-gradient(135deg, ${alpha(palette.main, 0.1)}, ${alpha(palette.light || palette.main, 0.05)})`,
167
+ borderColor: palette.main,
168
+ }),
169
+ ...(glow &&
170
+ selected && {
171
+ animation: `${glowAnimation} 2s ease-in-out infinite`,
172
+ boxShadow: `0 0 15px ${alpha(palette.main, 0.4)}`,
173
+ }),
174
+ };
175
+ };
176
+
177
+ /** The pill button's resting surface, before glass/gradient override parts of it. */
178
+ const buttonBase = (theme: Theme, flags: SurfaceFlags, palette: ColorPalette): CSSObject => {
179
+ const { selected, animated, customSize = 'md' } = flags;
180
+
181
+ return {
182
+ borderRadius: theme.spacing(1),
183
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
184
+ fontWeight: 500,
185
+ border: `2px solid ${selected ? palette.main : alpha(theme.palette.divider, 0.5)}`,
186
+ backgroundColor: selected ? palette.main : 'transparent',
187
+ color: selected ? palette.contrastText || '#fff' : theme.palette.text.primary,
188
+ position: 'relative' as const,
189
+ overflow: 'hidden' as const,
190
+ ...(animated && selected && { animation: `${scaleAnimation} 0.3s ease-out` }),
191
+ // Sized to zero until pressed, when the ripple keyframes expand it.
192
+ '&::after': {
193
+ content: '""',
194
+ position: 'absolute',
195
+ top: '50%',
196
+ left: '50%',
197
+ width: 0,
198
+ height: 0,
199
+ borderRadius: '50%',
200
+ backgroundColor: alpha(palette.contrastText || '#fff', 0.3),
201
+ transform: 'translate(-50%, -50%)',
202
+ pointerEvents: 'none' as const,
203
+ },
204
+ '&:active::after': {
205
+ animation: `${rippleAnimation} 0.6s ease-out`,
206
+ width: '100%',
207
+ height: '100%',
208
+ },
209
+ '&:hover': {
210
+ borderColor: palette.main,
211
+ backgroundColor: selected ? palette.dark : alpha(palette.main, 0.1),
212
+ transform: 'translateY(-1px) scale(1.02)',
213
+ boxShadow: `0 4px 12px ${alpha(palette.main, 0.2)}`,
214
+ },
215
+ '&:active': { transform: 'scale(0.98)' },
216
+ ...pickSize(BUTTON_SIZES, customSize),
217
+ };
218
+ };
219
+
220
+ export const buttonRadioSx = (theme: Theme, flags: SurfaceFlags): CSSObject => {
221
+ const { selected, customColor = 'primary', glass, gradient } = flags;
222
+ const palette = getColorFromTheme(theme, customColor);
223
+
224
+ return {
225
+ ...buttonBase(theme, flags, palette),
226
+ ...(glass && {
227
+ backgroundColor: selected
228
+ ? alpha(palette.main, 0.8)
229
+ : alpha(theme.palette.background.paper, 0.1),
230
+ backdropFilter: 'blur(20px)',
231
+ border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
232
+ }),
233
+ ...(gradient &&
234
+ selected && {
235
+ background: `linear-gradient(135deg, ${palette.main}, ${palette.dark})`,
236
+ border: 'none',
237
+ }),
238
+ };
239
+ };
240
+
241
+ export const segmentContainerSx = (
242
+ theme: Theme,
243
+ glass?: boolean,
244
+ customColor = 'primary',
245
+ ): CSSObject => {
246
+ const palette = getColorFromTheme(theme, customColor);
247
+
248
+ return {
249
+ padding: '4px',
250
+ borderRadius: theme.spacing(1.5),
251
+ backgroundColor: glass
252
+ ? alpha(theme.palette.background.paper, 0.1)
253
+ : alpha(palette.main, 0.05),
254
+ backdropFilter: glass ? 'blur(20px)' : 'none',
255
+ border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
256
+ display: 'flex',
257
+ gap: '2px',
258
+ };
259
+ };
260
+
261
+ /**
262
+ * An underline that grows from the centre outwards, and is already full width
263
+ * when the segment is the selected one.
264
+ */
265
+ const segmentUnderline = (color: string, selected: boolean): CSSObject => ({
266
+ content: '""',
267
+ position: 'absolute',
268
+ bottom: 0,
269
+ left: selected ? 0 : '50%',
270
+ width: selected ? '100%' : 0,
271
+ height: '2px',
272
+ backgroundColor: color,
273
+ transition: 'all 0.3s ease',
274
+ transform: selected ? 'translateX(0)' : 'translateX(-50%)',
275
+ });
276
+
277
+ export const segmentButtonSx = (theme: Theme, flags: SurfaceFlags): CSSObject => {
278
+ const { selected, customColor = 'primary', customSize = 'md', animated } = flags;
279
+ const palette = getColorFromTheme(theme, customColor);
280
+
281
+ return {
282
+ flex: 1,
283
+ borderRadius: theme.spacing(1),
284
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
285
+ fontWeight: 500,
286
+ backgroundColor: selected ? theme.palette.background.paper : 'transparent',
287
+ color: selected ? palette.main : theme.palette.text.secondary,
288
+ boxShadow: selected
289
+ ? `${theme.shadows[2]}, inset 0 1px 3px ${alpha(palette.main, 0.1)}`
290
+ : 'none',
291
+ position: 'relative' as const,
292
+ overflow: 'hidden' as const,
293
+ ...(animated && selected && { animation: `${scaleAnimation} 0.3s ease-out` }),
294
+ '&::before': segmentUnderline(palette.main, Boolean(selected)),
295
+ '&:hover': {
296
+ backgroundColor: selected
297
+ ? theme.palette.background.paper
298
+ : alpha(theme.palette.action.hover, 0.08),
299
+ color: palette.main,
300
+ '&::before': { width: '100%', left: 0, transform: 'translateX(0)' },
301
+ },
302
+ '&:active': { transform: 'scale(0.98)' },
303
+ ...pickSize(SEGMENT_SIZES, customSize),
304
+ };
305
+ };