feat: implement new SnackBar design (#5515)

Closes #5383

## Light theme

<img width="905" alt="image"
src="https://github.com/twentyhq/twenty/assets/3098428/ab0683c5-ded3-420c-ace6-684d38794a2d">

## Dark theme

<img width="903" alt="image"
src="https://github.com/twentyhq/twenty/assets/3098428/4e43ca35-438d-4ba0-8388-1f061c6ccfb0">
This commit is contained in:
Thaïs
2024-05-23 12:19:50 +02:00
committed by GitHub
parent 453525ca25
commit 8019ba8782
53 changed files with 485 additions and 552 deletions

View File

@ -1,110 +1,43 @@
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef,
} from 'react';
import { useTheme } from '@emotion/react';
import { useState } from 'react';
import styled from '@emotion/styled';
import { AnimationControls, motion, useAnimation } from 'framer-motion';
import { motion } from 'framer-motion';
export type ProgressBarProps = {
duration?: number;
delay?: number;
easing?: string;
barHeight?: number;
barColor?: string;
autoStart?: boolean;
className?: string;
color?: string;
value: number;
};
export type StyledBarProps = {
barHeight?: number;
className?: string;
};
export type ProgressBarControls = AnimationControls & {
start: () => Promise<any>;
pause: () => Promise<any>;
};
const StyledBar = styled.div<StyledBarProps>`
height: ${({ barHeight }) => barHeight}px;
height: ${({ theme }) => theme.spacing(2)};
overflow: hidden;
width: 100%;
`;
const StyledBarFilling = styled(motion.div)`
const StyledBarFilling = styled(motion.div)<{ color?: string }>`
background-color: ${({ color, theme }) => color ?? theme.font.color.primary};
height: 100%;
width: 100%;
`;
export const ProgressBar = forwardRef<ProgressBarControls, ProgressBarProps>(
(
{
duration = 3,
delay = 0,
easing = 'easeInOut',
barHeight = 24,
barColor,
autoStart = true,
className,
},
ref,
) => {
const theme = useTheme();
export const ProgressBar = ({ className, color, value }: ProgressBarProps) => {
const [initialValue] = useState(value);
const controls = useAnimation();
// eslint-disable-next-line @nx/workspace-no-state-useref
const startTimestamp = useRef<number>(0);
// eslint-disable-next-line @nx/workspace-no-state-useref
const remainingTime = useRef<number>(duration);
const start = useCallback(async () => {
startTimestamp.current = Date.now();
return controls.start({
scaleX: 0,
transition: {
duration: remainingTime.current / 1000, // convert ms to s for framer-motion
delay: delay / 1000, // likewise
ease: easing,
},
});
}, [controls, delay, easing]);
useImperativeHandle(ref, () => ({
...controls,
start: async () => {
return start();
},
pause: async () => {
const elapsed = Date.now() - startTimestamp.current;
remainingTime.current = remainingTime.current - elapsed;
return controls.stop();
},
}));
useEffect(() => {
if (autoStart) {
start();
}
}, [controls, delay, duration, easing, autoStart, start]);
return (
<StyledBar className={className} barHeight={barHeight}>
<StyledBarFilling
style={{
originX: 0,
// Seems like custom props are not well handled by react when used with framer-motion and emotion styled
backgroundColor: barColor ?? theme.color.gray80,
}}
initial={{ scaleX: 1 }}
animate={controls}
exit={{ scaleX: 0 }}
/>
</StyledBar>
);
},
);
return (
<StyledBar
className={className}
role="progressbar"
aria-valuenow={Math.ceil(value)}
>
<StyledBarFilling
initial={{ width: `${initialValue}%` }}
animate={{ width: `${value}%` }}
color={color}
transition={{ ease: 'linear' }}
/>
</StyledBar>
);
};

View File

@ -1,60 +1,49 @@
import { Meta, StoryObj } from '@storybook/react';
import { CatalogDecorator, CatalogStory, ComponentDecorator } from 'twenty-ui';
import { ComponentDecorator } from 'twenty-ui';
import { useProgressAnimation } from '@/ui/feedback/progress-bar/hooks/useProgressAnimation';
import { ProgressBar } from '../ProgressBar';
const meta: Meta<typeof ProgressBar> = {
title: 'UI/Feedback/ProgressBar/ProgressBar',
component: ProgressBar,
args: {
duration: 10000,
decorators: [ComponentDecorator],
argTypes: {
className: { control: false },
value: { control: { type: 'range', min: 0, max: 100, step: 1 } },
},
};
export default meta;
type Story = StoryObj<typeof ProgressBar>;
const args = {};
const defaultArgTypes = {
control: false,
};
export const Default: Story = {
args,
decorators: [ComponentDecorator],
args: {
value: 75,
},
};
export const Catalog: CatalogStory<Story, typeof ProgressBar> = {
args: {
...args,
},
export const Animated: Story = {
argTypes: {
barHeight: defaultArgTypes,
barColor: defaultArgTypes,
autoStart: defaultArgTypes,
value: { control: false },
},
parameters: {
catalog: {
dimensions: [
{
name: 'animation',
values: [true, false],
props: (autoStart: string) => ({ autoStart: Boolean(autoStart) }),
labels: (autoStart: string) => `AutoStart: ${autoStart}`,
decorators: [
(Story) => {
const { value } = useProgressAnimation({
autoPlay: true,
initialValue: 0,
finalValue: 100,
options: {
duration: 10000,
},
{
name: 'colors',
values: [undefined, 'blue'],
props: (barColor: string) => ({ barColor }),
labels: (color: string) => `Color: ${color ?? 'default'}`,
},
{
name: 'sizes',
values: [undefined, 10],
props: (barHeight: number) => ({ barHeight }),
labels: (size: number) => `Size: ${size ? size + ' px' : 'default'}`,
},
],
});
return <Story args={{ value }} />;
},
],
parameters: {
chromatic: { disableSnapshot: true },
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,58 @@
import { useCallback, useEffect, useState } from 'react';
import { millisecondsToSeconds } from 'date-fns';
import {
animate,
AnimationPlaybackControls,
ValueAnimationTransition,
} from 'framer-motion';
import { isDefined } from '~/utils/isDefined';
export const useProgressAnimation = ({
autoPlay = true,
initialValue = 0,
finalValue = 100,
options,
}: {
autoPlay?: boolean;
initialValue?: number;
finalValue?: number;
options?: ValueAnimationTransition<number>;
}) => {
const [animation, setAnimation] = useState<
AnimationPlaybackControls | undefined
>();
const [value, setValue] = useState(initialValue);
const startAnimation = useCallback(() => {
if (isDefined(animation)) return;
const duration = isDefined(options?.duration)
? millisecondsToSeconds(options.duration)
: undefined;
setAnimation(
animate(initialValue, finalValue, {
...options,
duration,
onUpdate: (nextValue) => {
if (value === nextValue) return;
setValue(nextValue);
options?.onUpdate?.(nextValue);
},
}),
);
}, [animation, finalValue, initialValue, options, value]);
useEffect(() => {
if (autoPlay && !animation) {
startAnimation();
}
}, [animation, autoPlay, startAnimation]);
return {
animation,
startAnimation,
value,
};
};