@linagora/twake-mui 9.12.0 → 9.14.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,17 @@
1
+ # @linagora/twake-mui [9.14.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@9.13.0...@linagora/twake-mui@9.14.0) (2026-09-29)
2
+
3
+
4
+ ### Features
5
+
6
+ * **twake-mui:** Migrate ExtendableFab from cozy-ui ([af9fb22](https://github.com/linagora/twake-ui/commit/af9fb22a17da746edb2cbb27f8a8bab0a2379fdc))
7
+
8
+ # @linagora/twake-mui [9.13.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@9.12.0...@linagora/twake-mui@9.13.0) (2026-09-29)
9
+
10
+
11
+ ### Features
12
+
13
+ * **twake-mui:** Migrate Empty from cozy-ui ([159c7e2](https://github.com/linagora/twake-ui/commit/159c7e2656c03989fed1c4ea6870dcf8889dd468))
14
+
1
15
  # @linagora/twake-mui [9.12.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@9.11.2...@linagora/twake-mui@9.12.0) (2026-09-29)
2
16
 
3
17
 
@@ -0,0 +1,7 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Empty } from './index';
3
+ declare const meta: Meta<typeof Empty>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof meta>;
6
+ export declare const Default: Story;
7
+ export declare const Screenshot: Story;
@@ -0,0 +1,52 @@
1
+ import { Icon, TwakeWorkplace } from '@linagora/twake-icons';
2
+ import { Box, Button, Stack } from '@mui/material';
3
+ import React from 'react';
4
+ import { Empty } from './index';
5
+ const iconSizes = ['normal', 'medium', 'large'];
6
+ const image = 'https://i.pravatar.cc/150?img=11';
7
+ const longTitle = 'This is a very very long title, just to see and verify how it works with it';
8
+ const longText = 'Ada Lovelace was an English mathematician and writer, chiefly known for her work on the Analytical Engine. She was the first to recognise that the machine had applications beyond pure calculation, and published the first algorithm intended to be carried out by such a machine.';
9
+ const meta = {
10
+ title: 'Empty',
11
+ component: Empty,
12
+ tags: ['autodocs'],
13
+ argTypes: {
14
+ iconSize: { control: 'select', options: iconSizes },
15
+ title: { control: 'text' },
16
+ text: { control: 'text' },
17
+ centered: { control: 'boolean' }
18
+ }
19
+ };
20
+ export default meta;
21
+ export const Default = {
22
+ args: {
23
+ icon: TwakeWorkplace,
24
+ iconSize: 'normal',
25
+ title: 'This list is empty',
26
+ text: 'Try adding some content to this list',
27
+ centered: false
28
+ }
29
+ };
30
+ const Section = ({ title, children }) => (React.createElement("section", null,
31
+ React.createElement("h3", { style: { marginBottom: '12px' } }, title),
32
+ children));
33
+ // Visual Regression - Combined view for Argos testing
34
+ export const Screenshot = {
35
+ tags: ['argos'],
36
+ render: () => (React.createElement(Stack, { spacing: 4 },
37
+ React.createElement(Section, { title: "Icon sizes" }, iconSizes.map(iconSize => (React.createElement(Empty, { key: iconSize, icon: TwakeWorkplace, iconSize: iconSize, title: "This list is empty", text: "Try adding some content to this list" })))),
38
+ React.createElement(Section, { title: "Long title and text" },
39
+ React.createElement(Empty, { icon: TwakeWorkplace, title: longTitle, text: longText })),
40
+ React.createElement(Section, { title: "With content" },
41
+ React.createElement(Empty, { icon: TwakeWorkplace, title: "This list is empty", text: "Try adding some content to this list" },
42
+ React.createElement(Button, { variant: "contained", sx: { mt: 2 } }, "Try refreshing"))),
43
+ React.createElement(Section, { title: "Centered" },
44
+ React.createElement(Box, { sx: { display: 'flex', height: 500, transform: 'scale(1)' } },
45
+ React.createElement(Empty, { icon: TwakeWorkplace, title: "This list is empty", text: "Try adding some content to this list", centered: true }))),
46
+ React.createElement(Section, { title: "Custom images" },
47
+ React.createElement(Empty, { icon: TwakeWorkplace, title: "With functional SVG", text: "Try adding some content to this list" }),
48
+ React.createElement(Empty, { icon: React.createElement("img", { src: image, alt: "" }), title: "With IMG", text: "Try adding some content to this list" }),
49
+ React.createElement(Empty, { icon: React.createElement("svg", { width: "100", height: "100" },
50
+ React.createElement("circle", { cx: "50", cy: "50", r: "40", stroke: "var(--twake-palette-primary-main)", strokeWidth: "4", fill: "var(--twake-palette-background-paper)" })), title: "With SVG", text: "Try adding some content to this list" }),
51
+ React.createElement(Empty, { icon: React.createElement(Icon, { icon: TwakeWorkplace }), title: "With Icon component", text: "Try adding some content to this list" }))))
52
+ };
@@ -0,0 +1,24 @@
1
+ import { IconProps } from '@linagora/twake-icons';
2
+ import { SxProps, TypographyProps } from '@mui/material';
3
+ import { Theme } from '@mui/material/styles';
4
+ import React from 'react';
5
+ export type EmptyIconSize = 'normal' | 'medium' | 'large';
6
+ export interface EmptyComponentsProps {
7
+ icon?: Partial<IconProps>;
8
+ title?: TypographyProps;
9
+ text?: TypographyProps;
10
+ childrenContainer?: React.ComponentPropsWithoutRef<'div'>;
11
+ }
12
+ export interface EmptyProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'title'> {
13
+ icon?: IconProps['icon'];
14
+ iconSize?: EmptyIconSize;
15
+ title?: React.ReactNode;
16
+ text?: React.ReactNode;
17
+ /** Below the `lg` breakpoint, pins the block to the centre of the viewport */
18
+ centered?: boolean;
19
+ componentsProps?: EmptyComponentsProps;
20
+ sx?: SxProps<Theme>;
21
+ }
22
+ export declare const EmptySubTitle: React.FC<TypographyProps>;
23
+ export declare const Empty: React.FC<EmptyProps>;
24
+ export default Empty;
@@ -0,0 +1,65 @@
1
+ import { Icon } from '@linagora/twake-icons';
2
+ import { Typography } from '@mui/material';
3
+ import { styled } from '@mui/material/styles';
4
+ import cx from 'classnames';
5
+ import React from 'react';
6
+ const ICON_CLASS_NAME = 'Empty-icon';
7
+ const iconHeights = {
8
+ normal: { desktop: 128, mobile: 96 },
9
+ medium: { desktop: 160, mobile: 128 },
10
+ large: { desktop: 192, mobile: 160 }
11
+ };
12
+ const EmptyRoot = styled('div', {
13
+ shouldForwardProp: prop => prop !== 'iconSize' && prop !== 'centered'
14
+ })(({ theme, iconSize, centered }) => ({
15
+ display: 'flex',
16
+ flexDirection: 'column',
17
+ justifyContent: 'center',
18
+ flex: '1 0 auto',
19
+ alignSelf: 'center',
20
+ margin: '0 auto',
21
+ boxSizing: 'content-box',
22
+ padding: theme.spacing(4),
23
+ textAlign: 'center',
24
+ width: `calc(100% - ${theme.spacing(8)})`,
25
+ maxWidth: 512,
26
+ [`& .${ICON_CLASS_NAME}`]: {
27
+ display: 'block',
28
+ margin: '0 auto 16px',
29
+ height: iconHeights[iconSize].desktop,
30
+ [theme.breakpoints.down('lg')]: {
31
+ marginBottom: 8,
32
+ height: iconHeights[iconSize].mobile
33
+ }
34
+ },
35
+ ...(centered && {
36
+ [theme.breakpoints.down('lg')]: {
37
+ position: 'fixed',
38
+ top: '50%',
39
+ left: '50%',
40
+ transform: 'translate(-50%, -50%)'
41
+ }
42
+ })
43
+ }));
44
+ const EmptyContent = styled('div')(({ theme }) => ({
45
+ maxWidth: 1008,
46
+ color: theme.vars.palette.text.secondary,
47
+ lineHeight: 1.5
48
+ }));
49
+ export const EmptySubTitle = props => (React.createElement(Typography, { variant: "body1", color: "textSecondary", ...props }));
50
+ const renderIcon = (icon, iconProps) => {
51
+ if (React.isValidElement(icon)) {
52
+ return React.cloneElement(icon, {
53
+ className: cx(ICON_CLASS_NAME, icon.props.className),
54
+ size: icon.props.size ?? (icon.type === Icon ? '100%' : undefined),
55
+ ...iconProps
56
+ });
57
+ }
58
+ return (React.createElement(Icon, { className: ICON_CLASS_NAME, icon: icon, size: "100%", ...iconProps }));
59
+ };
60
+ export const Empty = ({ icon, iconSize = 'normal', title, text, centered = false, componentsProps, children, ...props }) => (React.createElement(EmptyRoot, { iconSize: iconSize, centered: centered, ...props },
61
+ icon && renderIcon(icon, componentsProps?.icon),
62
+ title && (React.createElement(Typography, { gutterBottom: true, variant: "h3", color: "textPrimary", ...componentsProps?.title }, title)),
63
+ text && (React.createElement(EmptySubTitle, { gutterBottom: true, ...componentsProps?.text }, text)),
64
+ React.createElement(EmptyContent, { ...componentsProps?.childrenContainer }, children)));
65
+ export default Empty;
@@ -0,0 +1,7 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ExtendableFab } from './index';
3
+ declare const meta: Meta<typeof ExtendableFab>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof meta>;
6
+ export declare const Default: Story;
7
+ export declare const Screenshot: Story;
@@ -0,0 +1,43 @@
1
+ import { Plus } from '@linagora/twake-icons';
2
+ import { Box, Stack } from '@mui/material';
3
+ import React from 'react';
4
+ import { ExtendableFab } from './index';
5
+ const meta = {
6
+ title: 'ExtendableFab',
7
+ component: ExtendableFab,
8
+ parameters: { layout: 'fullscreen' },
9
+ tags: ['autodocs'],
10
+ args: {
11
+ label: 'Add',
12
+ icon: Plus,
13
+ color: 'primary',
14
+ size: 'medium',
15
+ topLimit: 50,
16
+ scrollOptions: { delay: 250 }
17
+ },
18
+ argTypes: {
19
+ label: { control: 'text' },
20
+ icon: { control: false },
21
+ color: {
22
+ control: 'select',
23
+ options: ['default', 'primary', 'secondary', 'inherit']
24
+ },
25
+ size: { control: 'select', options: ['small', 'medium', 'large'] },
26
+ topLimit: { control: 'number' },
27
+ scrollOptions: { control: 'object' },
28
+ disabled: { control: 'boolean' }
29
+ }
30
+ };
31
+ export default meta;
32
+ export const Default = {
33
+ render: args => (React.createElement(Box, { sx: { p: 2 } },
34
+ React.createElement(ExtendableFab, { sx: { position: 'sticky', top: 16 }, ...args }),
35
+ React.createElement(Box, { sx: { py: 2 } }, "Scroll the page vertically"),
36
+ React.createElement(Box, { sx: { height: '200vh' } })))
37
+ };
38
+ export const Screenshot = {
39
+ tags: ['argos'],
40
+ render: () => (React.createElement(Stack, { spacing: 2, direction: "row", sx: { p: 4 } },
41
+ React.createElement(ExtendableFab, { label: "Add", icon: Plus, color: "primary" }),
42
+ React.createElement(ExtendableFab, { label: "Add", icon: Plus, color: "inherit" })))
43
+ };
@@ -0,0 +1,16 @@
1
+ import { IconProps } from '@linagora/twake-icons';
2
+ import { FabProps } from '@mui/material';
3
+ import React from 'react';
4
+ export interface ScrollOptions {
5
+ /** Delay in ms before the window scroll position is read */
6
+ delay?: number;
7
+ }
8
+ export interface ExtendableFabProps extends Omit<FabProps, 'ref' | 'children'> {
9
+ label: string;
10
+ icon: IconProps['icon'];
11
+ /** Window scroll offset in px below which the Fab stays extended */
12
+ topLimit?: number;
13
+ scrollOptions?: ScrollOptions;
14
+ }
15
+ export declare const ExtendableFab: React.ForwardRefExoticComponent<ExtendableFabProps & React.RefAttributes<HTMLButtonElement>>;
16
+ export default ExtendableFab;
@@ -0,0 +1,20 @@
1
+ import { Icon } from '@linagora/twake-icons';
2
+ import { Fab, styled } from '@mui/material';
3
+ import React, { forwardRef, useEffect, useState } from 'react';
4
+ import { useDebounce } from 'rooks';
5
+ const GappedFab = styled(Fab)(({ theme }) => ({ gap: theme.spacing(1) }));
6
+ export const ExtendableFab = forwardRef(({ label, icon, topLimit = 50, scrollOptions, ...props }, ref) => {
7
+ const [isBelowTopLimit, setIsBelowTopLimit] = useState(true);
8
+ const handleScroll = useDebounce(() => {
9
+ setIsBelowTopLimit(window.scrollY < topLimit);
10
+ }, scrollOptions?.delay ?? 250);
11
+ useEffect(() => {
12
+ window.addEventListener('scroll', handleScroll, { passive: true });
13
+ return () => window.removeEventListener('scroll', handleScroll);
14
+ }, [handleScroll]);
15
+ return (React.createElement(GappedFab, { ref: ref, "aria-label": label, variant: isBelowTopLimit ? 'extended' : 'circular', ...props },
16
+ React.createElement(Icon, { icon: icon }),
17
+ isBelowTopLimit && label));
18
+ });
19
+ ExtendableFab.displayName = 'ExtendableFab';
20
+ export default ExtendableFab;
package/dist/index.d.ts CHANGED
@@ -13,6 +13,8 @@ export { Dialog, default as DialogDefault } from './components/Dialog';
13
13
  export { ContactPopover, default as ContactPopoverDefault } from './components/ContactPopover';
14
14
  export { default as DropdownButton } from './components/DropdownButton';
15
15
  export { default as DropdownText } from './components/DropdownText';
16
+ export { Empty, EmptySubTitle } from './components/Empty';
17
+ export { ExtendableFab } from './components/ExtendableFab';
16
18
  export { default as ListItem } from './components/ListItem';
17
19
  export { default as ListItemButton } from './components/ListItemButton';
18
20
  export { default as ListItemText } from './components/ListItemText';
@@ -49,8 +51,10 @@ export type { ContactPopoverCalendarActionProps } from './components/ContactPopo
49
51
  export type { ContactPopoverEmailActionProps } from './components/ContactPopover/ContactPopoverEmailAction';
50
52
  export type { ChipProps } from './components/Chip';
51
53
  export type { DialogProps, DialogSize } from './components/Dialog';
54
+ export type { EmptyProps, EmptyIconSize, EmptyComponentsProps } from './components/Empty';
52
55
  export type { DropdownButtonProps } from './components/DropdownButton';
53
56
  export type { DropdownTextProps, DropdownTextVariant } from './components/DropdownText';
57
+ export type { ExtendableFabProps, ScrollOptions } from './components/ExtendableFab';
54
58
  export type { ListItemProps, ListItemGutters, ListItemSize } from './components/ListItem';
55
59
  export type { ListItemButtonProps } from './components/ListItemButton';
56
60
  export type { ListItemTextProps } from './components/ListItemText';
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ export { Dialog, default as DialogDefault } from './components/Dialog';
16
16
  export { ContactPopover, default as ContactPopoverDefault } from './components/ContactPopover';
17
17
  export { default as DropdownButton } from './components/DropdownButton';
18
18
  export { default as DropdownText } from './components/DropdownText';
19
+ export { Empty, EmptySubTitle } from './components/Empty';
20
+ export { ExtendableFab } from './components/ExtendableFab';
19
21
  export { default as ListItem } from './components/ListItem';
20
22
  export { default as ListItemButton } from './components/ListItemButton';
21
23
  export { default as ListItemText } from './components/ListItemText';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linagora/twake-mui",
3
3
  "type": "module",
4
- "version": "9.12.0",
4
+ "version": "9.14.0",
5
5
  "description": "MUI v9 theme system for Twake applications",
6
6
  "author": "Twake",
7
7
  "license": "MIT",