@linagora/twake-mui 12.1.0 → 13.0.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/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # @linagora/twake-mui [13.0.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@12.1.0...@linagora/twake-mui@13.0.0) (2026-10-12)
2
+
3
+
4
+ ### chore
5
+
6
+ * **twake-mui:** Upgrade twake-i18n from 0.6.0 to 0.7.1 ([311b59b](https://github.com/linagora/twake-ui/commit/311b59b4ae9c9d1bb730a81d1c7df2d9311cf2dc))
7
+
8
+
9
+ ### Features
10
+
11
+ * **twake-mui:** Add ActionsBar locales ([ad935ba](https://github.com/linagora/twake-ui/commit/ad935bae17e8be853140507275b14f30a96035eb))
12
+ * **twake-mui:** Migrate ActionsBar from cozy-ui ([dcf2a17](https://github.com/linagora/twake-ui/commit/dcf2a17128be796c0f9c5aa0ebda36051a2a11ee))
13
+
14
+
15
+ ### BREAKING CHANGES
16
+
17
+ * **twake-mui:** twake-i18n >=0.7.1 is now a required peer dependency
18
+ of @linagora/twake-mui. Apps must upgrade twake-i18n to 0.7.1 or later.
19
+
1
20
  # @linagora/twake-mui [12.1.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@12.0.0...@linagora/twake-mui@12.1.0) (2026-10-12)
2
21
 
3
22
 
@@ -0,0 +1,19 @@
1
+ import { AppBarProps } from '@mui/material';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import React from 'react';
4
+ declare const icons: {
5
+ default: undefined;
6
+ star: () => React.JSX.Element;
7
+ flag: () => React.JSX.Element;
8
+ };
9
+ interface ExampleArgs {
10
+ color: AppBarProps['color'];
11
+ icon: keyof typeof icons;
12
+ maxDesktopActions: number;
13
+ autoClose: boolean;
14
+ }
15
+ declare const meta: Meta<ExampleArgs>;
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+ export declare const Default: Story;
19
+ export declare const Screenshot: Story;
@@ -0,0 +1,78 @@
1
+ import { FlagOutlined, Icon, Star } from '@linagora/twake-icons';
2
+ import { Box, Button, Stack } from '@mui/material';
3
+ import React, { useState } from 'react';
4
+ import ActionsBar from './index';
5
+ import { call } from '../ActionsMenu/Actions/call';
6
+ import { download } from '../ActionsMenu/Actions/download';
7
+ import { emailTo } from '../ActionsMenu/Actions/emailTo';
8
+ import { makeActions } from '../ActionsMenu/Actions/helpers';
9
+ import { modify } from '../ActionsMenu/Actions/modify';
10
+ import { smsTo } from '../ActionsMenu/Actions/smsTo';
11
+ const doc = {
12
+ _id: 'id01',
13
+ _type: 'io.cozy.contacts',
14
+ phone: [{ number: '0102030405' }],
15
+ email: [{ address: 'johndoe@example.com' }]
16
+ };
17
+ const actions = makeActions([modify, call, download, smsTo, emailTo], {
18
+ client: {
19
+ getStackClient: () => ({ uri: 'https://' }),
20
+ getInstanceOptions: () => ({ subdomain: '' })
21
+ },
22
+ generateWebLink: () => '',
23
+ downloadFile: () => undefined
24
+ });
25
+ const StarIcon = () => (React.createElement(Box, { component: "span", sx: { display: 'inline-flex', mr: 2 } },
26
+ React.createElement(Icon, { icon: Star })));
27
+ const FlagIcon = () => (React.createElement(Box, { component: "span", sx: { display: 'inline-flex', mr: 2 } },
28
+ React.createElement(Icon, { icon: FlagOutlined })));
29
+ const icons = { default: undefined, star: StarIcon, flag: FlagIcon };
30
+ const ExampleActionsBar = ({ color, icon, maxDesktopActions, autoClose }) => {
31
+ const [docs, setDocs] = useState([doc, doc]);
32
+ return (React.createElement(React.Fragment, null,
33
+ React.createElement(Stack, { direction: "row", spacing: 1 },
34
+ React.createElement(Button, { variant: "outlined", onClick: () => setDocs([...docs, doc]) }, "Add a selected document"),
35
+ React.createElement(Button, { variant: "outlined", onClick: () => setDocs(docs.slice(1)) }, "Remove a selected document")),
36
+ docs.length > 0 && (React.createElement(ActionsBar, { actions: actions, docs: docs, color: color, IconComponent: icons[icon], maxDesktopActions: maxDesktopActions, autoClose: autoClose, onClose: () => setDocs([]) }))));
37
+ };
38
+ const meta = {
39
+ title: 'ActionsBar',
40
+ tags: ['autodocs'],
41
+ parameters: { layout: 'fullscreen' },
42
+ argTypes: {
43
+ color: {
44
+ control: 'select',
45
+ options: [
46
+ 'default',
47
+ 'inherit',
48
+ 'primary',
49
+ 'secondary',
50
+ 'transparent',
51
+ 'error',
52
+ 'warning',
53
+ 'info',
54
+ 'success'
55
+ ]
56
+ },
57
+ icon: { control: 'select', options: Object.keys(icons) },
58
+ maxDesktopActions: { control: 'number' },
59
+ autoClose: { control: 'boolean' }
60
+ },
61
+ args: {
62
+ color: 'default',
63
+ icon: 'default',
64
+ maxDesktopActions: 5,
65
+ autoClose: true
66
+ },
67
+ render: args => (React.createElement(Box, { sx: { minHeight: 200, pt: 10, px: 2 } },
68
+ React.createElement(ExampleActionsBar, { ...args })))
69
+ };
70
+ export default meta;
71
+ export const Default = {};
72
+ // Visual Regression - the bar over two selected documents. Kept out of the
73
+ // docs page, where its fixed bar would cover the other stories
74
+ export const Screenshot = {
75
+ tags: ['argos', '!autodocs'],
76
+ render: () => (React.createElement(Box, { sx: { minHeight: 200 } },
77
+ React.createElement(ActionsBar, { actions: actions, docs: [doc, doc], onClose: () => undefined })))
78
+ };
@@ -0,0 +1,2 @@
1
+ declare const ActionsBarActions: import("@emotion/styled").StyledComponent<import("@mui/system").MUIStyledCommonProps<import("@mui/material/styles").Theme>, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
2
+ export default ActionsBarActions;
@@ -0,0 +1,14 @@
1
+ import { styled } from '@mui/material/styles';
2
+ const ActionsBarActions = styled('div')(({ theme }) => ({
3
+ display: 'flex',
4
+ flexGrow: 1,
5
+ alignItems: 'center',
6
+ justifyContent: 'end',
7
+ height: '100%',
8
+ paddingRight: 48,
9
+ [theme.breakpoints.down('md')]: {
10
+ justifyContent: 'center',
11
+ paddingRight: 0
12
+ }
13
+ }));
14
+ export default ActionsBarActions;
@@ -0,0 +1,2 @@
1
+ declare const ActionsBarRoot: import("@emotion/styled").StyledComponent<import("@mui/material").AppBarOwnProps & Omit<import("@mui/material").PaperOwnProps, "square" | "color" | "elevation" | "position" | "classes"> & import("@mui/material/OverridableComponent").CommonProps & Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLElement>, HTMLElement>, "style" | "square" | "color" | "className" | "elevation" | "children" | "position" | "classes" | "sx" | "variant" | "enableColorOnDark"> & import("@mui/system").MUIStyledCommonProps<import("@mui/material").Theme>, {}, {}>;
2
+ export default ActionsBarRoot;
@@ -0,0 +1,21 @@
1
+ import { AppBar } from '@mui/material';
2
+ import { styled } from '@mui/material/styles';
3
+ const ActionsBarRoot = styled(AppBar)(({ theme }) => ({
4
+ top: 0,
5
+ right: 22,
6
+ width: 'calc(100% - 44px)',
7
+ height: '3rem',
8
+ zIndex: 'var(--zIndex-selection)',
9
+ borderRadius: '0 0 16px 16px',
10
+ boxShadow: theme.vars.shadows[1],
11
+ [theme.breakpoints.down('md')]: {
12
+ top: 'auto',
13
+ bottom: 0,
14
+ right: 0,
15
+ width: '100%',
16
+ height: '4rem',
17
+ borderRadius: '16px 16px 0 0',
18
+ boxShadow: theme.vars.shadows[8]
19
+ }
20
+ }));
21
+ export default ActionsBarRoot;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ export interface ActionsBarSelectedCountProps {
3
+ count: number;
4
+ IconComponent?: React.ElementType;
5
+ onClose?: () => void;
6
+ }
7
+ declare const ActionsBarSelectedCount: ({ count, IconComponent, onClose }: ActionsBarSelectedCountProps) => React.JSX.Element;
8
+ export default ActionsBarSelectedCount;
@@ -0,0 +1,38 @@
1
+ import { CheckCircle, CrossCircle, Icon } from '@linagora/twake-icons';
2
+ import { Box, IconButton, Typography } from '@mui/material';
3
+ import React from 'react';
4
+ import { useExtendI18n, useI18n } from 'twake-i18n';
5
+ import de from './locales/de.json';
6
+ import en from './locales/en.json';
7
+ import es from './locales/es.json';
8
+ import fr from './locales/fr.json';
9
+ import it from './locales/it.json';
10
+ import ru from './locales/ru.json';
11
+ import vi from './locales/vi.json';
12
+ import { useBreakpoints } from '../../hooks/useBreakpoints';
13
+ const locales = { de, en, es, fr, it, ru, vi };
14
+ const DefaultIcon = () => (React.createElement(Box, { component: "span", sx: { display: 'inline-flex', mr: 2 } },
15
+ React.createElement(Icon, { icon: CheckCircle })));
16
+ const ActionsBarSelectedCount = ({ count, IconComponent = DefaultIcon, onClose }) => {
17
+ useExtendI18n(locales);
18
+ const { t } = useI18n();
19
+ const { isMobile } = useBreakpoints();
20
+ if (isMobile) {
21
+ return (React.createElement(Box, { sx: {
22
+ display: 'flex',
23
+ alignItems: 'center',
24
+ height: '100%',
25
+ pr: 2,
26
+ borderRadius: '15px 0 0 0',
27
+ bgcolor: 'background.paper',
28
+ color: 'text.primary'
29
+ } },
30
+ React.createElement(IconButton, { color: "inherit", onClick: onClose },
31
+ React.createElement(Icon, { icon: CrossCircle })),
32
+ count));
33
+ }
34
+ return (React.createElement(Box, { sx: { display: 'flex', alignItems: 'center', height: '100%', px: 2 } },
35
+ React.createElement(IconComponent, null),
36
+ React.createElement(Typography, { variant: "body1", component: "span", color: "inherit" }, t('ActionsBar.selected_light', { smart_count: count }))));
37
+ };
38
+ export default ActionsBarSelectedCount;
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ import { ActionComponentProps } from '../ActionsMenu/types';
3
+ /** Action of the bar, with its label under its icon on mobile */
4
+ declare const ResponsiveAction: React.ForwardRefExoticComponent<ActionComponentProps & React.RefAttributes<HTMLElement>>;
5
+ export default ResponsiveAction;
@@ -0,0 +1,19 @@
1
+ import { Icon } from '@linagora/twake-icons';
2
+ import { buttonClasses, Typography } from '@mui/material';
3
+ import React, { forwardRef } from 'react';
4
+ import { useBreakpoints } from '../../hooks/useBreakpoints';
5
+ import Button from '../Button';
6
+ const MOBILE_SX = {
7
+ flex: '1 1 0',
8
+ flexDirection: 'column',
9
+ padding: 0,
10
+ textTransform: 'none',
11
+ [`& .${buttonClasses.startIcon}`]: { marginRight: 0, marginBottom: '2px' }
12
+ };
13
+ /** Action of the bar, with its label under its icon on mobile */
14
+ const ResponsiveAction = forwardRef(({ action, disabled, onClick }, ref) => {
15
+ const { isMobile } = useBreakpoints();
16
+ return (React.createElement(Button, { ref: ref, variant: "text", color: "inherit", startIcon: action.icon && React.createElement(Icon, { icon: action.icon }), disabled: disabled, sx: isMobile ? MOBILE_SX : undefined, onClick: onClick }, isMobile ? (React.createElement(Typography, { variant: "caption", color: "inherit" }, action.label)) : (action.label)));
17
+ });
18
+ ResponsiveAction.displayName = 'ResponsiveAction';
19
+ export default ResponsiveAction;
@@ -0,0 +1,18 @@
1
+ import { AppBarProps } from '@mui/material';
2
+ import React from 'react';
3
+ import { ActionDocument, ActionObject } from '../ActionsMenu/types';
4
+ export interface ActionsBarProps {
5
+ actions: ActionObject[];
6
+ docs: ActionDocument[];
7
+ /** Closes the bar when an action is clicked */
8
+ autoClose?: boolean;
9
+ /** Actions shown in the bar on desktop, the others go to a menu */
10
+ maxDesktopActions?: number;
11
+ color?: AppBarProps['color'];
12
+ /** Replaces the icon next to the selection count, on desktop */
13
+ IconComponent?: React.ElementType;
14
+ onClose?: () => void;
15
+ }
16
+ /** Bar of the actions available on the selected documents */
17
+ declare const ActionsBar: ({ actions, docs, autoClose, maxDesktopActions, color, IconComponent, onClose }: ActionsBarProps) => React.JSX.Element;
18
+ export default ActionsBar;
@@ -0,0 +1,45 @@
1
+ import { Cross, Dots, Icon } from '@linagora/twake-icons';
2
+ import { IconButton, Toolbar } from '@mui/material';
3
+ import React, { useMemo, useRef, useState } from 'react';
4
+ import ActionsBarActions from './ActionsBarActions';
5
+ import ActionsBarRoot from './ActionsBarRoot';
6
+ import ActionsBarSelectedCount from './ActionsBarSelectedCount';
7
+ import ResponsiveAction from './ResponsiveAction';
8
+ import { useBreakpoints } from '../../hooks/useBreakpoints';
9
+ import ActionsMenu from '../ActionsMenu';
10
+ import { makeActions } from '../ActionsMenu/Actions/helpers';
11
+ import { others } from '../ActionsMenu/Actions/others';
12
+ import ActionsItems from '../ActionsMenu/ActionsItems';
13
+ import { getOnlyNeededActions } from '../ActionsMenu/helpers';
14
+ const MOBILE_MAX_ACTIONS = 4;
15
+ /** Bar of the actions available on the selected documents */
16
+ const ActionsBar = ({ actions, docs, autoClose = true, maxDesktopActions = 5, color = 'default', IconComponent, onClose }) => {
17
+ const { isMobile } = useBreakpoints();
18
+ const anchorRef = useRef(null);
19
+ const [showMenu, setShowMenu] = useState(false);
20
+ const maxActionsDisplayed = isMobile ? MOBILE_MAX_ACTIONS : maxDesktopActions;
21
+ const cleanedActions = useMemo(() => getOnlyNeededActions(actions, docs), [actions, docs]);
22
+ // Keeps a slot for the menu button when some actions do not fit
23
+ const countActionsToDisplay = cleanedActions.length > maxActionsDisplayed
24
+ ? maxActionsDisplayed - 1
25
+ : maxActionsDisplayed;
26
+ const sliceIndex = Math.max(1, countActionsToDisplay);
27
+ const barActions = cleanedActions.slice(0, sliceIndex);
28
+ const menuActions = cleanedActions.slice(sliceIndex);
29
+ const handleClick = () => {
30
+ if (autoClose)
31
+ onClose?.();
32
+ };
33
+ return (React.createElement(ActionsBarRoot, { position: "fixed", color: color },
34
+ React.createElement(Toolbar, { variant: isMobile ? 'regular' : 'dense', disableGutters: true, sx: { height: 'inherit' } },
35
+ !!onClose && !isMobile && (React.createElement(IconButton, { color: "inherit", sx: { position: 'absolute', right: 0 }, onClick: onClose },
36
+ React.createElement(Icon, { icon: Cross }))),
37
+ React.createElement(ActionsBarSelectedCount, { count: docs.length, IconComponent: IconComponent, onClose: onClose }),
38
+ React.createElement(ActionsBarActions, null,
39
+ React.createElement(ActionsItems, { docs: docs, actions: barActions, component: ResponsiveAction, onClick: handleClick }),
40
+ menuActions.length > 0 && (React.createElement(React.Fragment, null,
41
+ isMobile ? (React.createElement(ActionsItems, { docs: docs, actions: makeActions([others]), component: ResponsiveAction, onClick: () => setShowMenu(true) })) : (React.createElement(IconButton, { ref: anchorRef, color: "inherit", onClick: () => setShowMenu(true) },
42
+ React.createElement(Icon, { icon: Dots }))),
43
+ React.createElement(ActionsMenu, { ref: anchorRef, open: showMenu, docs: docs, actions: menuActions, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, componentsProps: { actionsItems: { onClick: handleClick } }, onClose: () => setShowMenu(false) })))))));
44
+ };
45
+ export default ActionsBar;
@@ -0,0 +1,6 @@
1
+ {
2
+ "ActionsBar": {
3
+ "selected": "%{smart_count} Element ausgewählt |||| %{smart_count} Elemente ausgewählt",
4
+ "selected_light": "%{smart_count} Element |||| %{smart_count} Elemente"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "ActionsBar": {
3
+ "selected": "%{smart_count} item selected |||| %{smart_count} items selected",
4
+ "selected_light": "%{smart_count} item |||| %{smart_count} items"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "ActionsBar": {
3
+ "selected": "%{smart_count} elemento seleccionado |||| %{smart_count} elementos seleccionados",
4
+ "selected_light": "%{smart_count} elemento |||| %{smart_count} elementos"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "ActionsBar": {
3
+ "selected": "%{smart_count} élément sélectionné |||| %{smart_count} éléments sélectionnés",
4
+ "selected_light": "%{smart_count} élément |||| %{smart_count} éléments"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "ActionsBar": {
3
+ "selected": "%{smart_count} elemento selezionato |||| %{smart_count} elementi selezionati",
4
+ "selected_light": "%{smart_count} elemento |||| %{smart_count} elementi"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "ActionsBar": {
3
+ "selected": "%{smart_count} элемент выбран |||| %{smart_count} элементов выбрано",
4
+ "selected_light": "%{smart_count} элемент |||| %{smart_count} элементов"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "ActionsBar": {
3
+ "selected": "%{smart_count} mục được chọn |||| %{smart_count} mục được chọn",
4
+ "selected_light": "%{smart_count} mục |||| %{smart_count} mục"
5
+ }
6
+ }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { makePalette } from './lib/makePalette';
6
6
  export { theme } from './lib/theme';
7
7
  export { radius } from './lib/radius';
8
8
  export { TwakeMuiThemeProvider } from './components/ThemeProvider';
9
+ export { default as ActionsBar } from './components/ActionsBar';
9
10
  export { default as ActionsMenu } from './components/ActionsMenu';
10
11
  export { default as ActionsMenuWrapper } from './components/ActionsMenu/ActionsMenuWrapper';
11
12
  export { default as ActionsMenuItem } from './components/ActionsMenu/ActionsMenuItem';
@@ -79,6 +80,7 @@ export { overlayClipPath, overlayRegionMessage, parseOverlayRegion, parseOverlay
79
80
  export { OverlayFrame } from './components/SpaceOverlay/OverlayFrame';
80
81
  export type { TwakeTheme } from './lib/theme';
81
82
  export type { PaletteJson } from './lib/types';
83
+ export type { ActionsBarProps } from './components/ActionsBar';
82
84
  export type { ActionsMenuProps, ActionsMenuComponentsProps } from './components/ActionsMenu';
83
85
  export type { ActionsMenuWrapperProps } from './components/ActionsMenu/ActionsMenuWrapper';
84
86
  export type { ActionsMenuItemProps } from './components/ActionsMenu/ActionsMenuItem';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export { theme } from './lib/theme';
9
9
  export { radius } from './lib/radius';
10
10
  // COMPONENTS & HELPERS
11
11
  export { TwakeMuiThemeProvider } from './components/ThemeProvider';
12
+ export { default as ActionsBar } from './components/ActionsBar';
12
13
  export { default as ActionsMenu } from './components/ActionsMenu';
13
14
  export { default as ActionsMenuWrapper } from './components/ActionsMenu/ActionsMenuWrapper';
14
15
  export { default as ActionsMenuItem } from './components/ActionsMenu/ActionsMenuItem';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linagora/twake-mui",
3
3
  "type": "module",
4
- "version": "12.1.0",
4
+ "version": "13.0.0",
5
5
  "description": "MUI v9 theme system for Twake applications",
6
6
  "author": "Twake",
7
7
  "license": "MIT",
@@ -57,7 +57,7 @@
57
57
  "cozy-intent": ">=2.29.1",
58
58
  "react": "^18 || ^19",
59
59
  "react-dom": "^18 || ^19",
60
- "twake-i18n": ">=0.6.0"
60
+ "twake-i18n": ">=0.7.1"
61
61
  },
62
62
  "dependencies": {
63
63
  "@mui/utils": "^9.2.0",
@@ -104,7 +104,7 @@
104
104
  "react": "^19.3.0",
105
105
  "react-dom": "^19.3.0",
106
106
  "storybook": "10.3.5",
107
- "twake-i18n": "^0.6.0",
107
+ "twake-i18n": "^0.7.1",
108
108
  "vite": "8.3.1",
109
109
  "vitest": "4.1.11"
110
110
  }