@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.
- package/eslint.config.js +12 -0
- package/package.json +5 -4
- package/src/components/data-display/AlertDialog/AlertDialog.test.stories.tsx +16 -6
- package/src/components/data-display/Banner/Banner.test.stories.tsx +26 -18
- package/src/components/data-display/Carousel/Carousel.chrome.tsx +83 -0
- package/src/components/data-display/Carousel/Carousel.hooks.ts +88 -0
- package/src/components/data-display/Carousel/Carousel.parts.tsx +346 -0
- package/src/components/data-display/Carousel/Carousel.slide.tsx +190 -0
- package/src/components/data-display/Carousel/Carousel.styles.ts +86 -0
- package/src/components/data-display/Carousel/Carousel.test.stories.tsx +15 -5
- package/src/components/data-display/Carousel/Carousel.tsx +84 -732
- package/src/components/data-display/HoverCard/HoverCard.animations.ts +73 -0
- package/src/components/data-display/HoverCard/HoverCard.content.tsx +143 -0
- package/src/components/data-display/HoverCard/HoverCard.hooks.ts +180 -0
- package/src/components/data-display/HoverCard/HoverCard.styles.ts +178 -0
- package/src/components/data-display/HoverCard/HoverCard.tsx +117 -631
- package/src/components/data-display/Sheet/Sheet.animations.ts +44 -0
- package/src/components/data-display/Sheet/Sheet.body.tsx +124 -0
- package/src/components/data-display/Sheet/Sheet.drag.ts +203 -0
- package/src/components/data-display/Sheet/Sheet.hooks.ts +318 -0
- package/src/components/data-display/Sheet/Sheet.parts.tsx +224 -0
- package/src/components/data-display/Sheet/Sheet.styles.ts +257 -0
- package/src/components/data-display/Sheet/Sheet.test.stories.tsx +25 -21
- package/src/components/data-display/Sheet/Sheet.tsx +125 -948
- package/src/components/feedback/Dialog/Dialog.test.stories.tsx +37 -28
- package/src/components/feedback/Modal/Modal.styles.ts +126 -0
- package/src/components/feedback/Modal/Modal.test.stories.tsx +5 -5
- package/src/components/feedback/Modal/Modal.transitions.tsx +40 -0
- package/src/components/feedback/Modal/Modal.tsx +37 -242
- package/src/components/feedback/StackedModal/StackedModal.test.stories.tsx +9 -11
- package/src/components/form/InputOTP/InputOTP.hooks.ts +104 -0
- package/src/components/form/InputOTP/InputOTP.styles.ts +97 -0
- package/src/components/form/InputOTP/InputOTP.tsx +74 -221
- package/src/components/form/Label/Label.parts.tsx +75 -0
- package/src/components/form/Label/Label.styles.ts +206 -0
- package/src/components/form/Label/Label.test.stories.tsx +20 -14
- package/src/components/form/Label/Label.tsx +73 -325
- package/src/components/form/Menubar/Menubar.hooks.ts +41 -0
- package/src/components/form/Menubar/Menubar.item.tsx +121 -0
- package/src/components/form/Menubar/Menubar.separator.tsx +25 -0
- package/src/components/form/Menubar/Menubar.styles.ts +110 -0
- package/src/components/form/Menubar/Menubar.toolbar.tsx +103 -0
- package/src/components/form/Menubar/Menubar.tsx +64 -347
- package/src/components/form/RadioGroup/RadioGroup.animations.ts +48 -0
- package/src/components/form/RadioGroup/RadioGroup.styles.ts +305 -0
- package/src/components/form/RadioGroup/RadioGroup.tsx +81 -651
- package/src/components/form/RadioGroup/RadioGroup.variants.tsx +291 -0
- package/src/components/form/Slider/Slider.animations.ts +40 -0
- package/src/components/form/Slider/Slider.styles.ts +292 -0
- package/src/components/form/Slider/Slider.tsx +106 -357
- package/src/components/form/Switch/Switch.animations.ts +62 -0
- package/src/components/form/Switch/Switch.parts.tsx +213 -0
- package/src/components/form/Switch/Switch.styles.ts +355 -0
- package/src/components/form/Switch/Switch.tsx +87 -566
- package/src/components/form/ToggleGroup/ToggleGroup.test.stories.tsx +26 -29
- package/src/components/layout/Resizable/Resizable.test.stories.tsx +11 -7
- package/src/components/layout/Spacer/Spacer.test.stories.tsx +11 -9
- package/src/components/typography/Blockquote/Blockquote.test.stories.tsx +7 -7
- package/src/components/typography/Heading/Heading.styles.ts +101 -0
- package/src/components/typography/Heading/Heading.tsx +48 -164
- package/src/components/typography/Text/Text.test.stories.tsx +27 -30
- package/src/components/utility/AnimatedIcon/AnimatedIcon.animations.ts +274 -0
- package/src/components/utility/AnimatedIcon/AnimatedIcon.styles.ts +231 -0
- package/src/components/utility/AnimatedIcon/AnimatedIcon.tsx +39 -559
- package/src/utils/index.ts +1 -0
- 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
|
+
};
|