Skip to content

Commit ea0a790

Browse files
committed
feat: implement new SnackBar design
Closes #5383
1 parent 474dfd7 commit ea0a790

File tree

16 files changed

+350
-342
lines changed

16 files changed

+350
-342
lines changed

packages/twenty-front/.storybook/preview.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ initialize({
2929
const preview: Preview = {
3030
decorators: [
3131
(Story) => {
32-
const mode = useDarkMode() ? 'Dark' : 'Light';
32+
const theme = useDarkMode() ? THEME_DARK : THEME_LIGHT;
3333

34-
const theme = mode === 'Dark' ? THEME_DARK : THEME_LIGHT;
3534
return (
3635
<ThemeProvider theme={theme}>
3736
<Story />
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,44 @@
1-
import {
2-
forwardRef,
3-
useCallback,
4-
useEffect,
5-
useImperativeHandle,
6-
useRef,
7-
} from 'react';
8-
import { useTheme } from '@emotion/react';
1+
import { useState } from 'react';
92
import styled from '@emotion/styled';
10-
import { AnimationControls, motion, useAnimation } from 'framer-motion';
3+
import { motion } from 'framer-motion';
114

125
export type ProgressBarProps = {
13-
duration?: number;
14-
delay?: number;
15-
easing?: string;
16-
barHeight?: number;
17-
barColor?: string;
18-
autoStart?: boolean;
196
className?: string;
7+
color?: string;
8+
value: number;
209
};
2110

2211
export type StyledBarProps = {
23-
barHeight?: number;
2412
className?: string;
2513
};
2614

27-
export type ProgressBarControls = AnimationControls & {
28-
start: () => Promise<any>;
29-
pause: () => Promise<any>;
30-
};
31-
3215
const StyledBar = styled.div<StyledBarProps>`
33-
height: ${({ barHeight }) => barHeight}px;
16+
height: ${({ theme }) => theme.spacing(2)};
3417
overflow: hidden;
3518
width: 100%;
3619
`;
3720

38-
const StyledBarFilling = styled(motion.div)`
21+
const StyledBarFilling = styled(motion.div)<{ color?: string }>`
22+
background-color: ${({ color, theme }) => color ?? theme.font.color.primary};
3923
height: 100%;
40-
width: 100%;
24+
transform-origin: 0;
4125
`;
4226

43-
export const ProgressBar = forwardRef<ProgressBarControls, ProgressBarProps>(
44-
(
45-
{
46-
duration = 3,
47-
delay = 0,
48-
easing = 'easeInOut',
49-
barHeight = 24,
50-
barColor,
51-
autoStart = true,
52-
className,
53-
},
54-
ref,
55-
) => {
56-
const theme = useTheme();
57-
58-
const controls = useAnimation();
59-
// eslint-disable-next-line @nx/workspace-no-state-useref
60-
const startTimestamp = useRef<number>(0);
61-
// eslint-disable-next-line @nx/workspace-no-state-useref
62-
const remainingTime = useRef<number>(duration);
63-
64-
const start = useCallback(async () => {
65-
startTimestamp.current = Date.now();
66-
return controls.start({
67-
scaleX: 0,
68-
transition: {
69-
duration: remainingTime.current / 1000, // convert ms to s for framer-motion
70-
delay: delay / 1000, // likewise
71-
ease: easing,
72-
},
73-
});
74-
}, [controls, delay, easing]);
75-
76-
useImperativeHandle(ref, () => ({
77-
...controls,
78-
start: async () => {
79-
return start();
80-
},
81-
pause: async () => {
82-
const elapsed = Date.now() - startTimestamp.current;
83-
84-
remainingTime.current = remainingTime.current - elapsed;
85-
return controls.stop();
86-
},
87-
}));
88-
89-
useEffect(() => {
90-
if (autoStart) {
91-
start();
92-
}
93-
}, [controls, delay, duration, easing, autoStart, start]);
94-
95-
return (
96-
<StyledBar className={className} barHeight={barHeight}>
97-
<StyledBarFilling
98-
style={{
99-
originX: 0,
100-
// Seems like custom props are not well handled by react when used with framer-motion and emotion styled
101-
backgroundColor: barColor ?? theme.color.gray80,
102-
}}
103-
initial={{ scaleX: 1 }}
104-
animate={controls}
105-
exit={{ scaleX: 0 }}
106-
/>
107-
</StyledBar>
108-
);
109-
},
110-
);
27+
export const ProgressBar = ({ className, color, value }: ProgressBarProps) => {
28+
const [initialValue] = useState(value);
29+
30+
return (
31+
<StyledBar
32+
className={className}
33+
role="progressbar"
34+
aria-valuenow={Math.ceil(value)}
35+
>
36+
<StyledBarFilling
37+
initial={{ width: `${initialValue}%` }}
38+
animate={{ width: `${value}%` }}
39+
color={color}
40+
transition={{ ease: 'linear' }}
41+
/>
42+
</StyledBar>
43+
);
44+
};
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,52 @@
11
import { Meta, StoryObj } from '@storybook/react';
2-
import { CatalogDecorator, CatalogStory, ComponentDecorator } from 'twenty-ui';
2+
import { ComponentDecorator, SECONDARY_COLORS } from 'twenty-ui';
3+
4+
import { useProgressAnimation } from '@/ui/feedback/progress-bar/hooks/useProgressAnimation';
35

46
import { ProgressBar } from '../ProgressBar';
57

68
const meta: Meta<typeof ProgressBar> = {
79
title: 'UI/Feedback/ProgressBar/ProgressBar',
810
component: ProgressBar,
11+
decorators: [ComponentDecorator],
12+
argTypes: {
13+
className: { control: false },
14+
value: { control: { type: 'range', min: 0, max: 100, step: 1 } },
15+
},
916
args: {
10-
duration: 10000,
17+
color: SECONDARY_COLORS.gray80,
1118
},
1219
};
1320

1421
export default meta;
1522

1623
type Story = StoryObj<typeof ProgressBar>;
17-
const args = {};
18-
const defaultArgTypes = {
19-
control: false,
20-
};
21-
export const Default: Story = {
22-
args,
23-
decorators: [ComponentDecorator],
24-
};
2524

26-
export const Catalog: CatalogStory<Story, typeof ProgressBar> = {
25+
export const Default: Story = {
2726
args: {
28-
...args,
27+
value: 75,
2928
},
29+
};
30+
31+
export const Animated: Story = {
3032
argTypes: {
31-
barHeight: defaultArgTypes,
32-
barColor: defaultArgTypes,
33-
autoStart: defaultArgTypes,
33+
value: { control: false },
3434
},
35-
parameters: {
36-
catalog: {
37-
dimensions: [
38-
{
39-
name: 'animation',
40-
values: [true, false],
41-
props: (autoStart: string) => ({ autoStart: Boolean(autoStart) }),
42-
labels: (autoStart: string) => `AutoStart: ${autoStart}`,
35+
decorators: [
36+
(Story) => {
37+
const { value } = useProgressAnimation({
38+
autoPlay: true,
39+
initialValue: 0,
40+
finalValue: 100,
41+
options: {
42+
duration: 10000,
4343
},
44-
{
45-
name: 'colors',
46-
values: [undefined, 'blue'],
47-
props: (barColor: string) => ({ barColor }),
48-
labels: (color: string) => `Color: ${color ?? 'default'}`,
49-
},
50-
{
51-
name: 'sizes',
52-
values: [undefined, 10],
53-
props: (barHeight: number) => ({ barHeight }),
54-
labels: (size: number) => `Size: ${size ? size + ' px' : 'default'}`,
55-
},
56-
],
44+
});
45+
46+
return <Story args={{ value }} />;
5747
},
48+
],
49+
parameters: {
50+
chromatic: { disableSnapshot: true },
5851
},
59-
decorators: [CatalogDecorator],
6052
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
import { millisecondsToSeconds } from 'date-fns';
3+
import {
4+
animate,
5+
AnimationPlaybackControls,
6+
ValueAnimationTransition,
7+
} from 'framer-motion';
8+
9+
import { isDefined } from '~/utils/isDefined';
10+
11+
export const useProgressAnimation = ({
12+
autoPlay = true,
13+
initialValue = 0,
14+
finalValue = 100,
15+
options,
16+
}: {
17+
autoPlay?: boolean;
18+
initialValue?: number;
19+
finalValue?: number;
20+
options?: ValueAnimationTransition<number>;
21+
}) => {
22+
const [animation, setAnimation] = useState<
23+
AnimationPlaybackControls | undefined
24+
>();
25+
const [value, setValue] = useState(initialValue);
26+
27+
const startAnimation = useCallback(() => {
28+
if (isDefined(animation)) return;
29+
30+
const duration = isDefined(options?.duration)
31+
? millisecondsToSeconds(options.duration)
32+
: undefined;
33+
34+
setAnimation(
35+
animate(initialValue, finalValue, {
36+
...options,
37+
duration,
38+
onUpdate: (nextValue) => {
39+
if (value === nextValue) return;
40+
setValue(nextValue);
41+
options?.onUpdate?.(nextValue);
42+
},
43+
}),
44+
);
45+
}, [animation, finalValue, initialValue, options, value]);
46+
47+
useEffect(() => {
48+
if (autoPlay && !animation) {
49+
startAnimation();
50+
}
51+
}, [animation, autoPlay, startAnimation]);
52+
53+
return {
54+
animation,
55+
startAnimation,
56+
value,
57+
};
58+
};

0 commit comments

Comments
 (0)