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:
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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],
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user