@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,41 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { MenubarItem, MenubarProps } from './Menubar.types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Which dropdown is open, and the buttons that opened them.
|
|
7
|
+
*
|
|
8
|
+
* The anchors are kept in a ref rather than state because closing needs to
|
|
9
|
+
* return focus to the button that opened the menu, and that lookup must not
|
|
10
|
+
* itself cause a render.
|
|
11
|
+
*/
|
|
12
|
+
export const useMenubarMenus = (onClick?: MenubarProps['onClick']) => {
|
|
13
|
+
const [activeMenu, setActiveMenu] = useState<string | null>(null);
|
|
14
|
+
const menuRefs = useRef<{ [key: string]: HTMLElement | null }>({});
|
|
15
|
+
|
|
16
|
+
const handleMenuOpen = useCallback((itemId: string, anchorEl: HTMLElement) => {
|
|
17
|
+
setActiveMenu(itemId);
|
|
18
|
+
menuRefs.current[itemId] = anchorEl;
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const handleMenuClose = useCallback(() => {
|
|
22
|
+
setActiveMenu((previous) => {
|
|
23
|
+
// Return focus to the button that opened the menu.
|
|
24
|
+
if (previous && menuRefs.current[previous]) {
|
|
25
|
+
menuRefs.current[previous].focus();
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
});
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const handleItemClick = useCallback(
|
|
32
|
+
(item: MenubarItem) => {
|
|
33
|
+
item.action?.();
|
|
34
|
+
onClick?.(item);
|
|
35
|
+
handleMenuClose();
|
|
36
|
+
},
|
|
37
|
+
[onClick, handleMenuClose],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return { activeMenu, menuRefs, handleMenuOpen, handleMenuClose, handleItemClick };
|
|
41
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
Chip,
|
|
6
|
+
Divider,
|
|
7
|
+
ListItemIcon,
|
|
8
|
+
ListItemText,
|
|
9
|
+
Menu,
|
|
10
|
+
MenuItem,
|
|
11
|
+
useTheme,
|
|
12
|
+
} from '@mui/material';
|
|
13
|
+
import React from 'react';
|
|
14
|
+
|
|
15
|
+
import { sizeStyles } from './Menubar.styles';
|
|
16
|
+
import type { MenubarItem, MenubarProps } from './Menubar.types';
|
|
17
|
+
|
|
18
|
+
export interface MenubarItemViewProps {
|
|
19
|
+
item: MenubarItem;
|
|
20
|
+
size: NonNullable<MenubarProps['size']>;
|
|
21
|
+
disabled: boolean;
|
|
22
|
+
isOpen: boolean;
|
|
23
|
+
anchorEl: HTMLElement | null;
|
|
24
|
+
onOpen: (itemId: string, anchorEl: HTMLElement) => void;
|
|
25
|
+
onActivate: (item: MenubarItem) => void;
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
onFocus?: React.FocusEventHandler;
|
|
28
|
+
onBlur?: React.FocusEventHandler;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One row of an open menu: its icon, its label, and its shortcut hint. */
|
|
32
|
+
const DropdownItem: React.FC<{ child: MenubarItem; onActivate: (item: MenubarItem) => void }> = ({
|
|
33
|
+
child,
|
|
34
|
+
onActivate,
|
|
35
|
+
}) =>
|
|
36
|
+
child.divider ? (
|
|
37
|
+
<Divider />
|
|
38
|
+
) : (
|
|
39
|
+
<MenuItem onClick={() => onActivate(child)} disabled={child.disabled}>
|
|
40
|
+
{child.icon && <ListItemIcon>{child.icon}</ListItemIcon>}
|
|
41
|
+
<ListItemText>{child.label}</ListItemText>
|
|
42
|
+
{child.shortcut && (
|
|
43
|
+
<Chip
|
|
44
|
+
label={child.shortcut}
|
|
45
|
+
size="small"
|
|
46
|
+
variant="outlined"
|
|
47
|
+
sx={{ ml: 2, height: 20, fontSize: '0.7rem' }}
|
|
48
|
+
/>
|
|
49
|
+
)}
|
|
50
|
+
</MenuItem>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A top-level menubar entry. An entry with children opens its dropdown; one
|
|
55
|
+
* without acts immediately — a decision the click and keyboard paths both need,
|
|
56
|
+
* so it is made once here rather than in each handler.
|
|
57
|
+
*/
|
|
58
|
+
export const MenubarItemView: React.FC<MenubarItemViewProps> = ({
|
|
59
|
+
item,
|
|
60
|
+
size,
|
|
61
|
+
disabled,
|
|
62
|
+
isOpen,
|
|
63
|
+
anchorEl,
|
|
64
|
+
onOpen,
|
|
65
|
+
onActivate,
|
|
66
|
+
onClose,
|
|
67
|
+
onFocus,
|
|
68
|
+
onBlur,
|
|
69
|
+
}) => {
|
|
70
|
+
const theme = useTheme();
|
|
71
|
+
const hasChildren = Boolean(item.children && item.children.length > 0);
|
|
72
|
+
|
|
73
|
+
const activate = (target: HTMLElement) => {
|
|
74
|
+
if (hasChildren) {
|
|
75
|
+
onOpen(item.id, target);
|
|
76
|
+
} else {
|
|
77
|
+
onActivate(item);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Box>
|
|
83
|
+
<Button
|
|
84
|
+
data-testid={`menubar-button-${item.id}`}
|
|
85
|
+
onClick={(e) => activate(e.currentTarget)}
|
|
86
|
+
onKeyDown={(e) => {
|
|
87
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
activate(e.currentTarget);
|
|
90
|
+
}
|
|
91
|
+
}}
|
|
92
|
+
// Keeping focus on mousedown is what lets handleMenuClose return it here.
|
|
93
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
94
|
+
onFocus={onFocus}
|
|
95
|
+
onBlur={onBlur}
|
|
96
|
+
disabled={item.disabled || disabled}
|
|
97
|
+
startIcon={item.icon}
|
|
98
|
+
endIcon={hasChildren ? <KeyboardArrowDownIcon /> : null}
|
|
99
|
+
aria-haspopup={hasChildren ? 'menu' : undefined}
|
|
100
|
+
aria-expanded={hasChildren ? isOpen : undefined}
|
|
101
|
+
sx={{ color: 'inherit', textTransform: 'none', ...sizeStyles(theme, size) }}
|
|
102
|
+
>
|
|
103
|
+
{item.label}
|
|
104
|
+
</Button>
|
|
105
|
+
|
|
106
|
+
{hasChildren && (
|
|
107
|
+
<Menu
|
|
108
|
+
anchorEl={anchorEl}
|
|
109
|
+
open={isOpen}
|
|
110
|
+
onClose={onClose}
|
|
111
|
+
autoFocus
|
|
112
|
+
PaperProps={{ sx: { minWidth: 200, mt: 1 } }}
|
|
113
|
+
>
|
|
114
|
+
{item.children?.map((child) => (
|
|
115
|
+
<DropdownItem key={child.id} child={child} onActivate={onActivate} />
|
|
116
|
+
))}
|
|
117
|
+
</Menu>
|
|
118
|
+
)}
|
|
119
|
+
</Box>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Divider } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { MenubarSeparatorProps } from './Menubar.types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The rule between two entries. Its own orientation is the bar's turned ninety
|
|
8
|
+
* degrees: a horizontal bar is divided by vertical rules.
|
|
9
|
+
*/
|
|
10
|
+
export const MenubarSeparator: React.FC<MenubarSeparatorProps> = ({
|
|
11
|
+
orientation = 'horizontal',
|
|
12
|
+
className,
|
|
13
|
+
style,
|
|
14
|
+
}) => (
|
|
15
|
+
<Divider
|
|
16
|
+
orientation={orientation === 'horizontal' ? 'vertical' : 'horizontal'}
|
|
17
|
+
flexItem
|
|
18
|
+
className={className}
|
|
19
|
+
sx={{
|
|
20
|
+
mx: orientation === 'horizontal' ? 1 : 0,
|
|
21
|
+
my: orientation === 'vertical' ? 1 : 0,
|
|
22
|
+
...style,
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { alpha } from '@mui/material';
|
|
2
|
+
import type { CSSObject, Theme } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
import type { MenubarProps } from './Menubar.types';
|
|
5
|
+
|
|
6
|
+
type Size = NonNullable<MenubarProps['size']>;
|
|
7
|
+
type Variant = NonNullable<MenubarProps['variant']>;
|
|
8
|
+
type Color = NonNullable<MenubarProps['color']>;
|
|
9
|
+
|
|
10
|
+
const SIZES: Record<Size, (theme: Theme) => CSSObject> = {
|
|
11
|
+
xs: (theme) => ({ minHeight: 40, fontSize: '0.75rem', padding: theme.spacing(0.5, 1) }),
|
|
12
|
+
sm: (theme) => ({ minHeight: 48, fontSize: '0.875rem', padding: theme.spacing(0.75, 1.5) }),
|
|
13
|
+
md: (theme) => ({ minHeight: 56, fontSize: '1rem', padding: theme.spacing(1, 2) }),
|
|
14
|
+
lg: (theme) => ({ minHeight: 64, fontSize: '1.125rem', padding: theme.spacing(1.25, 2.5) }),
|
|
15
|
+
xl: (theme) => ({ minHeight: 72, fontSize: '1.25rem', padding: theme.spacing(1.5, 3) }),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const sizeStyles = (theme: Theme, size: Size): CSSObject =>
|
|
19
|
+
(SIZES[size] ?? SIZES.md)(theme);
|
|
20
|
+
|
|
21
|
+
/** `default` is not a palette entry — it means the paper surface and its text. */
|
|
22
|
+
const accentOf = (theme: Theme, color: Color) =>
|
|
23
|
+
color === 'default' ? theme.palette.primary.main : theme.palette[color].main;
|
|
24
|
+
|
|
25
|
+
const colorStyles = (theme: Theme, color: Color, transparent: boolean): CSSObject => {
|
|
26
|
+
const surface =
|
|
27
|
+
color === 'default'
|
|
28
|
+
? { backgroundColor: theme.palette.background.paper, color: theme.palette.text.primary }
|
|
29
|
+
: { backgroundColor: theme.palette[color].main, color: theme.palette[color].contrastText };
|
|
30
|
+
|
|
31
|
+
return transparent ? { ...surface, backgroundColor: 'transparent' } : surface;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface MenubarStyleFlags {
|
|
35
|
+
variant: Variant;
|
|
36
|
+
size: Size;
|
|
37
|
+
color: Color;
|
|
38
|
+
glow: boolean;
|
|
39
|
+
pulse: boolean;
|
|
40
|
+
glass: boolean;
|
|
41
|
+
gradient: boolean;
|
|
42
|
+
blur: boolean;
|
|
43
|
+
transparent: boolean;
|
|
44
|
+
elevation: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Each variant's own additions to the shared bar. The `glass` and `gradient`
|
|
49
|
+
* arms deliberately do not take the blur spread: glass sets its own
|
|
50
|
+
* backdropFilter, and a blurred gradient loses the gradient.
|
|
51
|
+
*/
|
|
52
|
+
const SURFACES: Record<Variant, (theme: Theme, flags: MenubarStyleFlags) => CSSObject> = {
|
|
53
|
+
glass: (theme, { glass }) => ({
|
|
54
|
+
backgroundColor: alpha(theme.palette.background.paper, glass ? 0.1 : 0.8),
|
|
55
|
+
backdropFilter: 'blur(20px)',
|
|
56
|
+
borderBottom: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
gradient: (theme, { color, gradient }) => {
|
|
60
|
+
const key = color === 'default' ? 'primary' : color;
|
|
61
|
+
return {
|
|
62
|
+
background: gradient
|
|
63
|
+
? `linear-gradient(135deg, ${theme.palette[key].light}, ${theme.palette[key].dark})`
|
|
64
|
+
: theme.palette[key].main,
|
|
65
|
+
color: theme.palette[key].contrastText,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
elevated: (theme, { blur, elevation }) => ({
|
|
70
|
+
...(blur && { backdropFilter: 'blur(10px)' }),
|
|
71
|
+
boxShadow: theme.shadows[elevation],
|
|
72
|
+
}),
|
|
73
|
+
|
|
74
|
+
minimal: (_theme, { blur }) => ({
|
|
75
|
+
...(blur && { backdropFilter: 'blur(10px)' }),
|
|
76
|
+
boxShadow: 'none',
|
|
77
|
+
borderBottom: 'none',
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
bordered: (theme, { blur, color }) => ({
|
|
81
|
+
...(blur && { backdropFilter: 'blur(10px)' }),
|
|
82
|
+
borderBottom: `2px solid ${
|
|
83
|
+
color === 'default' ? theme.palette.divider : theme.palette[color].main
|
|
84
|
+
}`,
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
default: (_theme, { blur }) => ({ ...(blur && { backdropFilter: 'blur(10px)' }) }),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const barStyles = (theme: Theme, flags: MenubarStyleFlags): CSSObject => {
|
|
91
|
+
const { variant, size, color, glow, pulse, transparent } = flags;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
transition: theme.transitions.create(['all'], {
|
|
95
|
+
duration: theme.transitions.duration.standard,
|
|
96
|
+
}),
|
|
97
|
+
...sizeStyles(theme, size),
|
|
98
|
+
...colorStyles(theme, color, transparent),
|
|
99
|
+
...(glow && { boxShadow: `0 0 20px ${alpha(accentOf(theme, color), 0.4)}` }),
|
|
100
|
+
...(pulse && {
|
|
101
|
+
animation: 'pulse 2s infinite',
|
|
102
|
+
'@keyframes pulse': {
|
|
103
|
+
'0%': { opacity: 1 },
|
|
104
|
+
'50%': { opacity: 0.8 },
|
|
105
|
+
'100%': { opacity: 1 },
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
...(SURFACES[variant] ?? SURFACES.default)(theme, flags),
|
|
109
|
+
};
|
|
110
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Box, CircularProgress, Toolbar } from '@mui/material';
|
|
2
|
+
import type { CSSObject } from '@mui/material';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { MenubarItemView } from './Menubar.item';
|
|
6
|
+
import { MenubarSeparator } from './Menubar.separator';
|
|
7
|
+
import type { MenubarItem, MenubarProps } from './Menubar.types';
|
|
8
|
+
|
|
9
|
+
export interface MenubarToolbarProps {
|
|
10
|
+
barSx: CSSObject;
|
|
11
|
+
items: MenubarItem[];
|
|
12
|
+
size: NonNullable<MenubarProps['size']>;
|
|
13
|
+
orientation: NonNullable<MenubarProps['orientation']>;
|
|
14
|
+
disabled: boolean;
|
|
15
|
+
loading: boolean;
|
|
16
|
+
logo?: React.ReactNode;
|
|
17
|
+
endContent?: React.ReactNode;
|
|
18
|
+
fullWidth: boolean;
|
|
19
|
+
activeMenu: string | null;
|
|
20
|
+
menuRefs: React.MutableRefObject<{ [key: string]: HTMLElement | null }>;
|
|
21
|
+
onOpen: (itemId: string, anchorEl: HTMLElement) => void;
|
|
22
|
+
onActivate: (item: MenubarItem) => void;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
onFocus?: React.FocusEventHandler;
|
|
25
|
+
onBlur?: React.FocusEventHandler;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The bar's contents: an optional logo, the entries themselves, and whatever the
|
|
30
|
+
* caller hangs off the end. Shared by both orientations — only the element
|
|
31
|
+
* wrapping it differs.
|
|
32
|
+
*/
|
|
33
|
+
export const MenubarToolbar: React.FC<MenubarToolbarProps> = ({
|
|
34
|
+
barSx,
|
|
35
|
+
items,
|
|
36
|
+
size,
|
|
37
|
+
orientation,
|
|
38
|
+
disabled,
|
|
39
|
+
loading,
|
|
40
|
+
logo,
|
|
41
|
+
endContent,
|
|
42
|
+
fullWidth,
|
|
43
|
+
activeMenu,
|
|
44
|
+
menuRefs,
|
|
45
|
+
onOpen,
|
|
46
|
+
onActivate,
|
|
47
|
+
onClose,
|
|
48
|
+
onFocus,
|
|
49
|
+
onBlur,
|
|
50
|
+
}) => {
|
|
51
|
+
const isVertical = orientation === 'vertical';
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Toolbar
|
|
55
|
+
role="toolbar"
|
|
56
|
+
aria-label="Main navigation"
|
|
57
|
+
sx={{
|
|
58
|
+
...barSx,
|
|
59
|
+
width: fullWidth ? '100%' : 'auto',
|
|
60
|
+
flexDirection: isVertical ? 'column' : 'row',
|
|
61
|
+
alignItems: isVertical ? 'flex-start' : 'center',
|
|
62
|
+
gap: 1,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{logo && <Box sx={{ mr: 2, display: 'flex', alignItems: 'center' }}>{logo}</Box>}
|
|
66
|
+
|
|
67
|
+
{loading ? (
|
|
68
|
+
<CircularProgress size={20} color="inherit" />
|
|
69
|
+
) : (
|
|
70
|
+
<Box
|
|
71
|
+
sx={{
|
|
72
|
+
display: 'flex',
|
|
73
|
+
flexDirection: isVertical ? 'column' : 'row',
|
|
74
|
+
gap: isVertical ? 0.5 : 1,
|
|
75
|
+
flex: 1,
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{items.map((item) =>
|
|
79
|
+
item.divider ? (
|
|
80
|
+
<MenubarSeparator key={item.id} orientation={orientation} />
|
|
81
|
+
) : (
|
|
82
|
+
<MenubarItemView
|
|
83
|
+
key={item.id}
|
|
84
|
+
item={item}
|
|
85
|
+
size={size}
|
|
86
|
+
disabled={disabled}
|
|
87
|
+
isOpen={activeMenu === item.id}
|
|
88
|
+
anchorEl={menuRefs.current[item.id] ?? null}
|
|
89
|
+
onOpen={onOpen}
|
|
90
|
+
onActivate={onActivate}
|
|
91
|
+
onClose={onClose}
|
|
92
|
+
onFocus={onFocus}
|
|
93
|
+
onBlur={onBlur}
|
|
94
|
+
/>
|
|
95
|
+
),
|
|
96
|
+
)}
|
|
97
|
+
</Box>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{endContent && <Box sx={{ ml: 'auto' }}>{endContent}</Box>}
|
|
101
|
+
</Toolbar>
|
|
102
|
+
);
|
|
103
|
+
};
|