@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,357 +1,91 @@
1
1
  import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
2
2
  import {
3
- alpha,
4
3
  AppBar,
5
4
  Box,
6
5
  Button,
7
- Chip,
8
- CircularProgress,
9
6
  Divider,
10
7
  ListItemIcon,
11
8
  ListItemText,
12
9
  Menu,
13
10
  MenuItem,
14
- Toolbar,
15
11
  Typography,
16
12
  useTheme,
17
13
  } from '@mui/material';
18
- import React, { useRef,useState } from 'react';
14
+ import React, { useState } from 'react';
19
15
 
16
+ import { withDefaults } from '../../../utils/withDefaults';
17
+
18
+ import { useMenubarMenus } from './Menubar.hooks';
19
+ import { barStyles } from './Menubar.styles';
20
+ import { MenubarToolbar } from './Menubar.toolbar';
20
21
  import type {
21
22
  MenubarGroupProps,
22
23
  MenubarItem,
23
24
  MenubarProps,
24
- MenubarSeparatorProps,
25
25
  } from './Menubar.types';
26
26
 
27
- export const Menubar: React.FC<MenubarProps> = ({
28
- items,
29
- variant = 'default',
30
- size = 'md',
31
- color = 'default',
32
- orientation = 'horizontal',
33
- glow = false,
34
- pulse = false,
35
- glass = false,
36
- gradient = false,
37
- loading = false,
38
- disabled = false,
39
- className,
40
- style,
41
- logo,
42
- endContent,
43
- sticky = false,
44
- transparent = false,
45
- blur = false,
46
- elevation = 4,
47
- fullWidth = true,
48
- onClick,
49
- onFocus,
50
- onBlur,
51
- 'data-testid': dataTestId,
52
- }) => {
53
- const theme = useTheme();
54
- const [activeMenu, setActiveMenu] = useState<string | null>(null);
55
- const menuRefs = useRef<{ [key: string]: HTMLElement | null }>({});
56
-
57
- const handleMenuOpen = (itemId: string, anchorEl: HTMLElement) => {
58
- setActiveMenu(itemId);
59
- menuRefs.current[itemId] = anchorEl;
60
- };
61
-
62
- const handleMenuClose = () => {
63
- const previousMenu = activeMenu;
64
- setActiveMenu(null);
65
- // Return focus to the button that opened the menu
66
- if (previousMenu && menuRefs.current[previousMenu]) {
67
- menuRefs.current[previousMenu].focus();
68
- }
69
- };
70
-
71
- const handleItemClick = (item: MenubarItem) => {
72
- if (item.action) {
73
- item.action();
74
- }
75
- onClick?.(item);
76
- handleMenuClose();
77
- };
78
-
79
- const getSizeStyles = () => {
80
- switch (size) {
81
- case 'xs':
82
- return {
83
- minHeight: 40,
84
- fontSize: '0.75rem',
85
- padding: theme.spacing(0.5, 1),
86
- };
87
- case 'sm':
88
- return {
89
- minHeight: 48,
90
- fontSize: '0.875rem',
91
- padding: theme.spacing(0.75, 1.5),
92
- };
93
- case 'md':
94
- return {
95
- minHeight: 56,
96
- fontSize: '1rem',
97
- padding: theme.spacing(1, 2),
98
- };
99
- case 'lg':
100
- return {
101
- minHeight: 64,
102
- fontSize: '1.125rem',
103
- padding: theme.spacing(1.25, 2.5),
104
- };
105
- case 'xl':
106
- return {
107
- minHeight: 72,
108
- fontSize: '1.25rem',
109
- padding: theme.spacing(1.5, 3),
110
- };
111
- default:
112
- return {
113
- minHeight: 56,
114
- fontSize: '1rem',
115
- padding: theme.spacing(1, 2),
116
- };
117
- }
118
- };
119
-
120
- const getColorStyles = () => {
121
- if (color === 'default') {
122
- return {
123
- backgroundColor: transparent ? 'transparent' : theme.palette.background.paper,
124
- color: theme.palette.text.primary,
125
- };
126
- }
127
- return {
128
- backgroundColor: transparent ? 'transparent' : theme.palette[color].main,
129
- color: theme.palette[color].contrastText,
130
- };
131
- };
132
-
133
- const getVariantStyles = () => {
134
- const baseStyles = {
135
- transition: theme.transitions.create(['all'], {
136
- duration: theme.transitions.duration.standard,
137
- }),
138
- ...getSizeStyles(),
139
- ...getColorStyles(),
140
- };
141
-
142
- const glowStyles = glow
143
- ? {
144
- boxShadow: `0 0 20px ${alpha(
145
- color === 'default' ? theme.palette.primary.main : theme.palette[color].main,
146
- 0.4,
147
- )}`,
148
- }
149
- : {};
150
-
151
- const pulseStyles = pulse
152
- ? {
153
- animation: 'pulse 2s infinite',
154
- '@keyframes pulse': {
155
- '0%': { opacity: 1 },
156
- '50%': { opacity: 0.8 },
157
- '100%': { opacity: 1 },
158
- },
159
- }
160
- : {};
27
+ const DEFAULTS = {
28
+ variant: 'default',
29
+ size: 'md',
30
+ color: 'default',
31
+ orientation: 'horizontal',
32
+ glow: false,
33
+ pulse: false,
34
+ glass: false,
35
+ gradient: false,
36
+ loading: false,
37
+ disabled: false,
38
+ sticky: false,
39
+ transparent: false,
40
+ blur: false,
41
+ elevation: 4,
42
+ fullWidth: true,
43
+ } satisfies Partial<MenubarProps>;
44
+
45
+ type ResolvedProps = MenubarProps & Required<Pick<MenubarProps, keyof typeof DEFAULTS>>;
46
+
47
+ export const Menubar: React.FC<MenubarProps> = (props) => {
48
+ const {
49
+ items, variant, size, color, orientation, glow, pulse, glass, gradient,
50
+ loading, disabled, className, style, logo, endContent, sticky, transparent,
51
+ blur, elevation, fullWidth, onClick, onFocus, onBlur,
52
+ 'data-testid': dataTestId,
53
+ } = withDefaults(props, DEFAULTS) as ResolvedProps;
161
54
 
162
- const blurStyles = blur
163
- ? {
164
- backdropFilter: 'blur(10px)',
165
- }
166
- : {};
167
-
168
- switch (variant) {
169
- case 'glass':
170
- return {
171
- ...baseStyles,
172
- ...glowStyles,
173
- ...pulseStyles,
174
- backgroundColor: alpha(theme.palette.background.paper, glass ? 0.1 : 0.8),
175
- backdropFilter: 'blur(20px)',
176
- borderBottom: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
177
- };
178
-
179
- case 'gradient': {
180
- const gradientColor = color === 'default' ? 'primary' : color;
181
- return {
182
- ...baseStyles,
183
- ...glowStyles,
184
- ...pulseStyles,
185
- background: gradient
186
- ? `linear-gradient(135deg, ${theme.palette[gradientColor].light}, ${theme.palette[gradientColor].dark})`
187
- : theme.palette[gradientColor].main,
188
- color: theme.palette[gradientColor].contrastText,
189
- };
190
- }
191
-
192
- case 'elevated':
193
- return {
194
- ...baseStyles,
195
- ...glowStyles,
196
- ...pulseStyles,
197
- ...blurStyles,
198
- boxShadow: theme.shadows[elevation],
199
- };
200
-
201
- case 'minimal':
202
- return {
203
- ...baseStyles,
204
- ...glowStyles,
205
- ...pulseStyles,
206
- ...blurStyles,
207
- boxShadow: 'none',
208
- borderBottom: 'none',
209
- };
210
-
211
- case 'bordered':
212
- return {
213
- ...baseStyles,
214
- ...glowStyles,
215
- ...pulseStyles,
216
- ...blurStyles,
217
- borderBottom: `2px solid ${
218
- color === 'default' ? theme.palette.divider : theme.palette[color].main
219
- }`,
220
- };
221
-
222
- default:
223
- return {
224
- ...baseStyles,
225
- ...glowStyles,
226
- ...pulseStyles,
227
- ...blurStyles,
228
- };
229
- }
230
- };
55
+ const theme = useTheme();
56
+ const { activeMenu, menuRefs, handleMenuOpen, handleMenuClose, handleItemClick } =
57
+ useMenubarMenus(onClick);
231
58
 
232
- const renderMenubarItem = (item: MenubarItem) => {
233
- if (item.divider) {
234
- return <MenubarSeparator key={item.id} orientation={orientation} />;
235
- }
59
+ const isVertical = orientation === 'vertical';
236
60
 
237
- const hasChildren = item.children && item.children.length > 0;
238
-
239
- return (
240
- <Box key={item.id}>
241
- <Button
242
- data-testid={`menubar-button-${item.id}`}
243
- onClick={(e) => {
244
- if (hasChildren) {
245
- handleMenuOpen(item.id, e.currentTarget);
246
- } else {
247
- handleItemClick(item);
248
- }
249
- }}
250
- onKeyDown={(e) => {
251
- if (e.key === 'Enter' || e.key === ' ') {
252
- e.preventDefault();
253
- if (hasChildren) {
254
- handleMenuOpen(item.id, e.currentTarget);
255
- } else {
256
- handleItemClick(item);
257
- }
258
- }
259
- }}
260
- onMouseDown={(e) => {
261
- // Prevent default to maintain focus
262
- e.preventDefault();
263
- }}
264
- onFocus={onFocus}
265
- onBlur={onBlur}
266
- disabled={item.disabled || disabled}
267
- startIcon={item.icon}
268
- endIcon={hasChildren ? <KeyboardArrowDownIcon /> : null}
269
- aria-haspopup={hasChildren ? 'menu' : undefined}
270
- aria-expanded={hasChildren ? activeMenu === item.id : undefined}
271
- sx={{
272
- color: 'inherit',
273
- textTransform: 'none',
274
- ...getSizeStyles(),
275
- }}
276
- >
277
- {item.label}
278
- </Button>
279
-
280
- {hasChildren && (
281
- <Menu
282
- anchorEl={menuRefs.current[item.id]}
283
- open={activeMenu === item.id}
284
- onClose={handleMenuClose}
285
- autoFocus
286
- PaperProps={{
287
- sx: {
288
- minWidth: 200,
289
- mt: 1,
290
- },
291
- }}
292
- >
293
- {item.children?.map((child) =>
294
- child.divider ? (
295
- <Divider key={child.id} />
296
- ) : (
297
- <MenuItem
298
- key={child.id}
299
- onClick={() => handleItemClick(child)}
300
- disabled={child.disabled}
301
- >
302
- {child.icon && <ListItemIcon>{child.icon}</ListItemIcon>}
303
- <ListItemText>{child.label}</ListItemText>
304
- {child.shortcut && (
305
- <Chip
306
- label={child.shortcut}
307
- size="small"
308
- variant="outlined"
309
- sx={{ ml: 2, height: 20, fontSize: '0.7rem' }}
310
- />
311
- )}
312
- </MenuItem>
313
- ),
314
- )}
315
- </Menu>
316
- )}
317
- </Box>
318
- );
319
- };
61
+ const barSx = barStyles(theme, {
62
+ variant, size, color, glow, pulse, glass, gradient, blur, transparent, elevation,
63
+ });
320
64
 
321
65
  const content = (
322
- <Toolbar
323
- role="toolbar"
324
- aria-label="Main navigation"
325
- sx={{
326
- ...getVariantStyles(),
327
- width: fullWidth ? '100%' : 'auto',
328
- flexDirection: orientation === 'vertical' ? 'column' : 'row',
329
- alignItems: orientation === 'vertical' ? 'flex-start' : 'center',
330
- gap: 1,
331
- }}
332
- >
333
- {logo && <Box sx={{ mr: 2, display: 'flex', alignItems: 'center' }}>{logo}</Box>}
334
-
335
- {loading ? (
336
- <CircularProgress size={20} color="inherit" />
337
- ) : (
338
- <Box
339
- sx={{
340
- display: 'flex',
341
- flexDirection: orientation === 'vertical' ? 'column' : 'row',
342
- gap: orientation === 'vertical' ? 0.5 : 1,
343
- flex: 1,
344
- }}
345
- >
346
- {items.map(renderMenubarItem)}
347
- </Box>
348
- )}
349
-
350
- {endContent && <Box sx={{ ml: 'auto' }}>{endContent}</Box>}
351
- </Toolbar>
66
+ <MenubarToolbar
67
+ barSx={barSx}
68
+ items={items}
69
+ size={size}
70
+ orientation={orientation}
71
+ disabled={disabled}
72
+ loading={loading}
73
+ logo={logo}
74
+ endContent={endContent}
75
+ fullWidth={fullWidth}
76
+ activeMenu={activeMenu}
77
+ menuRefs={menuRefs}
78
+ onOpen={handleMenuOpen}
79
+ onActivate={handleItemClick}
80
+ onClose={handleMenuClose}
81
+ onFocus={onFocus}
82
+ onBlur={onBlur}
83
+ />
352
84
  );
353
85
 
354
- if (orientation === 'vertical') {
86
+ // A vertical bar is not an app bar: AppBar's fixed/sticky positioning assumes
87
+ // a horizontal strip at the top of the page.
88
+ if (isVertical) {
355
89
  return (
356
90
  <Box
357
91
  className={className}
@@ -372,10 +106,7 @@ export const Menubar: React.FC<MenubarProps> = ({
372
106
  position={sticky ? 'sticky' : 'static'}
373
107
  className={className}
374
108
  data-testid={dataTestId}
375
- sx={{
376
- ...getVariantStyles(),
377
- ...style,
378
- }}
109
+ sx={{ ...barSx, ...style }}
379
110
  elevation={0}
380
111
  component="header"
381
112
  >
@@ -384,6 +115,7 @@ export const Menubar: React.FC<MenubarProps> = ({
384
115
  );
385
116
  };
386
117
 
118
+
387
119
  export const MenubarGroup: React.FC<MenubarGroupProps> = ({
388
120
  label,
389
121
  items,
@@ -449,19 +181,4 @@ export const MenubarGroup: React.FC<MenubarGroupProps> = ({
449
181
  );
450
182
  };
451
183
 
452
- export const MenubarSeparator: React.FC<MenubarSeparatorProps> = ({
453
- orientation = 'horizontal',
454
- className,
455
- style,
456
- }) => (
457
- <Divider
458
- orientation={orientation === 'horizontal' ? 'vertical' : 'horizontal'}
459
- flexItem
460
- className={className}
461
- sx={{
462
- mx: orientation === 'horizontal' ? 1 : 0,
463
- my: orientation === 'vertical' ? 1 : 0,
464
- ...style,
465
- }}
466
- />
467
- );
184
+ export { MenubarSeparator } from './Menubar.separator';
@@ -0,0 +1,48 @@
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 slideAnimation = keyframes`
16
+ from {
17
+ opacity: 0;
18
+ transform: translateX(-20px);
19
+ }
20
+ to {
21
+ opacity: 1;
22
+ transform: translateX(0);
23
+ }
24
+ `;
25
+
26
+ export const scaleAnimation = keyframes`
27
+ 0% {
28
+ transform: scale(0.95);
29
+ }
30
+ 50% {
31
+ transform: scale(1.05);
32
+ }
33
+ 100% {
34
+ transform: scale(1);
35
+ }
36
+ `;
37
+
38
+ export const rippleAnimation = keyframes`
39
+ 0% {
40
+ transform: scale(0);
41
+ opacity: 1;
42
+ }
43
+ 100% {
44
+ transform: scale(4);
45
+ opacity: 0;
46
+ }
47
+ `;
48
+