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,
};
};

View File

@ -1,184 +1,192 @@
import { useCallback, useMemo, useRef } from 'react';
import { ComponentPropsWithoutRef, ReactNode, useMemo } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconAlertTriangle, IconX } from 'twenty-ui';
import { isUndefined } from '@sniptt/guards';
import {
ProgressBar,
ProgressBarControls,
} from '@/ui/feedback/progress-bar/components/ProgressBar';
import { RGBA } from '@/ui/theme/constants/Rgba';
IconAlertTriangle,
IconInfoCircle,
IconSquareRoundedCheck,
IconX,
} from 'twenty-ui';
import { ProgressBar } from '@/ui/feedback/progress-bar/components/ProgressBar';
import { useProgressAnimation } from '@/ui/feedback/progress-bar/hooks/useProgressAnimation';
import { LightButton } from '@/ui/input/button/components/LightButton';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { isDefined } from '~/utils/isDefined';
import { usePausableTimeout } from '../hooks/usePausableTimeout';
export enum SnackBarVariant {
Default = 'default',
Error = 'error',
Success = 'success',
Info = 'info',
Warning = 'warning',
}
const StyledMotionContainer = styled.div<Pick<SnackBarProps, 'variant'>>`
align-items: center;
background-color: ${({ theme, variant }) => {
switch (variant) {
case 'error':
return theme.snackBar.error.background;
case 'success':
return theme.snackBar.success.background;
case 'info':
default:
return theme.color.gray80;
}
}};
border-radius: ${({ theme }) => theme.border.radius.sm};
export type SnackBarProps = Pick<
ComponentPropsWithoutRef<'div'>,
'id' | 'title'
> & {
className?: string;
progress?: number;
duration?: number;
icon?: ReactNode;
message?: string;
onCancel?: () => void;
onClose?: () => void;
role?: 'alert' | 'status';
variant?: SnackBarVariant;
};
const StyledContainer = styled.div`
backdrop-filter: ${({ theme }) => theme.blur.light};
background-color: ${({ theme }) => theme.background.transparent.primary};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
color: ${({ theme, variant }) => {
switch (variant) {
case 'error':
return theme.snackBar.error.color;
case 'success':
return theme.snackBar.success.color;
case 'info':
default:
return theme.grayScale.gray0;
}
}};
box-sizing: border-box;
cursor: pointer;
display: flex;
height: 40px;
overflow: hidden;
height: 61px;
padding: ${({ theme }) => theme.spacing(2)};
pointer-events: auto;
position: relative;
width: 296px;
`;
const StyledIconContainer = styled.div`
display: flex;
margin-right: ${({ theme }) => theme.spacing(2)};
`;
const StyledProgressBarContainer = styled.div`
height: 5px;
const StyledProgressBar = styled(ProgressBar)`
bottom: 0;
height: auto;
left: 0;
position: absolute;
right: 0;
top: 0;
pointer-events: none;
`;
const StyledCloseButton = styled.button<Pick<SnackBarProps, 'variant'>>`
const StyledHeader = styled.div`
align-items: center;
background-color: transparent;
border: none;
border-radius: 12px;
color: ${({ theme, variant }) => {
switch (variant) {
case 'error':
return theme.color.red20;
case 'success':
return theme.color.turquoise20;
case 'info':
default:
return theme.grayScale.gray0;
}
}};
cursor: pointer;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
height: 24px;
justify-content: center;
margin-left: ${({ theme }) => theme.spacing(6)};
padding-left: ${({ theme }) => theme.spacing(1)};
padding-right: ${({ theme }) => theme.spacing(1)};
width: 24px;
&:hover {
background-color: ${({ theme }) => RGBA(theme.grayScale.gray0, 0.1)};
}
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(2)};
height: ${({ theme }) => theme.spacing(6)};
margin-bottom: ${({ theme }) => theme.spacing(1)};
`;
export type SnackbarVariant = 'info' | 'error' | 'success';
const StyledActions = styled.div`
align-items: center;
display: flex;
margin-left: auto;
`;
export interface SnackBarProps extends React.ComponentPropsWithoutRef<'div'> {
role?: 'alert' | 'status';
icon?: React.ReactNode;
message?: string;
allowDismiss?: boolean;
duration?: number;
variant?: SnackbarVariant;
children?: React.ReactNode;
className?: string;
onClose?: () => void;
}
const StyledDescription = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
padding-left: ${({ theme }) => theme.spacing(6)};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 200px;
`;
const defaultTitleByVariant: Record<SnackBarVariant, string> = {
[SnackBarVariant.Default]: 'Alert',
[SnackBarVariant.Error]: 'Error',
[SnackBarVariant.Info]: 'Info',
[SnackBarVariant.Success]: 'Success',
[SnackBarVariant.Warning]: 'Warning',
};
export const SnackBar = ({
role = 'status',
icon: iconComponent,
message,
allowDismiss = true,
duration = 6000,
variant = 'info',
children,
onClose,
id,
title,
className,
progress: overrideProgressValue,
duration = 6000,
icon: iconComponent,
id,
message,
onCancel,
onClose,
role = 'status',
variant = SnackBarVariant.Default,
title = defaultTitleByVariant[variant],
}: SnackBarProps) => {
const theme = useTheme();
// eslint-disable-next-line @nx/workspace-no-state-useref
const progressBarRef = useRef<ProgressBarControls | null>(null);
const closeSnackbar = useCallback(() => {
onClose && onClose();
}, [onClose]);
const { pauseTimeout, resumeTimeout } = usePausableTimeout(
closeSnackbar,
duration,
);
const { animation: progressAnimation, value: progressValue } =
useProgressAnimation({
autoPlay: isUndefined(overrideProgressValue),
initialValue: isDefined(overrideProgressValue)
? overrideProgressValue
: 100,
finalValue: 0,
options: { duration, onComplete: onClose },
});
const icon = useMemo(() => {
if (isDefined(iconComponent)) {
return iconComponent;
}
switch (variant) {
case 'error':
return (
<IconAlertTriangle aria-label="Error" size={theme.icon.size.md} />
);
case 'success':
case 'info':
default:
return null;
}
}, [iconComponent, theme.icon.size.md, variant]);
const ariaLabel = defaultTitleByVariant[variant];
const color = theme.snackBar[variant].color;
const size = theme.icon.size.md;
const onMouseEnter = () => {
progressBarRef.current?.pause();
pauseTimeout();
switch (variant) {
case SnackBarVariant.Error:
return (
<IconAlertTriangle {...{ 'aria-label': ariaLabel, color, size }} />
);
case SnackBarVariant.Info:
return <IconInfoCircle {...{ 'aria-label': ariaLabel, color, size }} />;
case SnackBarVariant.Success:
return (
<IconSquareRoundedCheck
{...{ 'aria-label': ariaLabel, color, size }}
/>
);
case SnackBarVariant.Warning:
return (
<IconAlertTriangle {...{ 'aria-label': ariaLabel, color, size }} />
);
default:
return (
<IconAlertTriangle {...{ 'aria-label': ariaLabel, color, size }} />
);
}
}, [iconComponent, theme.icon.size.md, theme.snackBar, variant]);
const handleMouseEnter = () => {
if (progressAnimation?.state === 'running') {
progressAnimation.pause();
}
};
const onMouseLeave = () => {
progressBarRef.current?.start();
resumeTimeout();
const handleMouseLeave = () => {
if (progressAnimation?.state === 'paused') {
progressAnimation.play();
}
};
return (
<StyledMotionContainer
className={className}
<StyledContainer
aria-live={role === 'alert' ? 'assertive' : 'polite'}
{...{ id, onMouseEnter, onMouseLeave, role, title, variant }}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
title={message || title || defaultTitleByVariant[variant]}
{...{ className, id, role, variant }}
>
<StyledProgressBarContainer>
<ProgressBar
ref={progressBarRef}
barHeight={5}
barColor={RGBA(theme.grayScale.gray0, 0.3)}
duration={duration}
/>
</StyledProgressBarContainer>
{icon && <StyledIconContainer>{icon}</StyledIconContainer>}
{children ? children : message}
{allowDismiss && (
<StyledCloseButton variant={variant} onClick={closeSnackbar}>
<IconX aria-label="Close" size={theme.icon.size.md} />
</StyledCloseButton>
)}
</StyledMotionContainer>
<StyledProgressBar
color={theme.snackBar[variant].backgroundColor}
value={progressValue}
/>
<StyledHeader>
{icon}
{title}
<StyledActions>
{!!onCancel && <LightButton title="Cancel" onClick={onCancel} />}
{!!onClose && (
<LightIconButton title="Close" Icon={IconX} onClick={onClose} />
)}
</StyledActions>
</StyledHeader>
{message && <StyledDescription>{message}</StyledDescription>}
</StyledContainer>
);
};

View File

@ -1,57 +1,39 @@
import styled from '@emotion/styled';
import { motion, useReducedMotion } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { MOBILE_VIEWPORT } from 'twenty-ui';
import { useSnackBarManagerScopedStates } from '@/ui/feedback/snack-bar-manager/hooks/internal/useSnackBarManagerScopedStates';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SnackBar } from './SnackBar';
const StyledSnackBarContainer = styled.div`
display: flex;
flex-direction: column;
position: fixed;
right: 0;
top: 0;
z-index: 99999999;
`;
right: ${({ theme }) => theme.spacing(3)};
bottom: ${({ theme }) => theme.spacing(3)};
z-index: ${({ theme }) => theme.lastLayerZIndex};
const StyledSnackBarMotionContainer = styled(motion.div)`
margin-right: ${({ theme }) => theme.spacing(3)};
margin-top: ${({ theme }) => theme.spacing(3)};
@media (max-width: ${MOBILE_VIEWPORT}px) {
bottom: ${({ theme }) => theme.spacing(16)};
right: 50%;
transform: translateX(50%);
}
`;
const variants = {
initial: {
out: {
opacity: 0,
y: -40,
y: 40,
},
animate: {
in: {
opacity: 1,
y: 0,
},
exit: {
opacity: 0,
y: -40,
},
};
const reducedVariants = {
initial: {
opacity: 0,
y: -40,
},
animate: {
opacity: 1,
y: 0,
},
exit: {
opacity: 0,
y: -40,
},
};
export const SnackBarProvider = ({ children }: React.PropsWithChildren) => {
const reducedMotion = useReducedMotion();
const { snackBarInternal } = useSnackBarManagerScopedStates();
const { handleSnackBarClose } = useSnackBar();
@ -59,24 +41,26 @@ export const SnackBarProvider = ({ children }: React.PropsWithChildren) => {
<>
{children}
<StyledSnackBarContainer>
{snackBarInternal.queue.map(
({ duration, icon, id, message, title, variant }) => (
<StyledSnackBarMotionContainer
key={id}
variants={reducedMotion ? reducedVariants : variants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.5 }}
layout
>
<SnackBar
{...{ duration, icon, message, title, variant }}
onClose={() => handleSnackBarClose(id)}
/>
</StyledSnackBarMotionContainer>
),
)}
<AnimatePresence>
{snackBarInternal.queue.map(
({ duration, icon, id, message, title, variant }) => (
<motion.div
key={id}
variants={variants}
initial="out"
animate="in"
exit="out"
transition={{ duration: 0.5 }}
layout
>
<SnackBar
{...{ duration, icon, message, title, variant }}
onClose={() => handleSnackBarClose(id)}
/>
</motion.div>
),
)}
</AnimatePresence>
</StyledSnackBarContainer>
</>
);

View File

@ -0,0 +1,63 @@
import { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import {
CatalogDecorator,
CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { SnackBar, SnackBarVariant } from '../SnackBar';
const meta: Meta<typeof SnackBar> = {
title: 'UI/Feedback/SnackBarManager/SnackBar',
component: SnackBar,
decorators: [SnackBarDecorator],
argTypes: {
className: { control: false },
icon: { control: false },
},
args: {
title: 'Lorem ipsum',
message:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec eros tincidunt lacinia.',
onCancel: undefined,
onClose: fn(),
role: 'status',
variant: SnackBarVariant.Default,
},
};
export default meta;
type Story = StoryObj<typeof SnackBar>;
export const Default: Story = {
decorators: [ComponentDecorator],
parameters: {
chromatic: { disableSnapshot: true },
},
};
export const Catalog: CatalogStory<Story, typeof SnackBar> = {
args: {
onCancel: fn(),
},
decorators: [CatalogDecorator],
parameters: {
catalog: {
dimensions: [
{
name: 'progress',
values: [0, 75, 100],
props: (progress) => ({ progress }),
},
{
name: 'variants',
values: Object.values(SnackBarVariant),
props: (variant: SnackBarVariant) => ({ variant }),
},
],
},
},
};

View File

@ -1,47 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { usePausableTimeout } from '@/ui/feedback/snack-bar-manager/hooks/usePausableTimeout';
jest.useFakeTimers();
describe('usePausableTimeout', () => {
it('should pause and resume timeout', () => {
let callbackExecuted = false;
const callback = () => {
callbackExecuted = true;
};
const { result } = renderHook(() => usePausableTimeout(callback, 1000));
// timetravel 500ms into the future
act(() => {
jest.advanceTimersByTime(500);
});
expect(callbackExecuted).toBe(false);
act(() => {
result.current.pauseTimeout();
});
// timetravel another 500ms into the future
act(() => {
jest.advanceTimersByTime(500);
});
// The callback should not have been executed while paused
expect(callbackExecuted).toBe(false);
act(() => {
result.current.resumeTimeout();
});
// advance all timers controlled by Jest to their final state
act(() => {
jest.runAllTimers();
});
// The callback should now have been executed
expect(callbackExecuted).toBe(true);
});
});

View File

@ -1,56 +0,0 @@
import { useCallback, useEffect, useRef } from 'react';
import { isDefined } from '~/utils/isDefined';
export const usePausableTimeout = (callback: () => void, delay: number) => {
// eslint-disable-next-line @nx/workspace-no-state-useref
const savedCallback = useRef<() => void>(callback);
// eslint-disable-next-line @nx/workspace-no-state-useref
const remainingTime = useRef<number>(delay);
// eslint-disable-next-line @nx/workspace-no-state-useref
const startTime = useRef<number>(Date.now());
// eslint-disable-next-line @nx/workspace-no-state-useref
const timeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
const tick = () => {
if (isDefined(savedCallback.current)) {
savedCallback.current();
}
};
const startTimeout = useCallback(() => {
startTime.current = Date.now();
timeoutId.current = setTimeout(tick, remainingTime.current);
}, []);
// Remember the latest callback
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the timeout loop
useEffect(() => {
if (delay !== null) {
startTimeout();
return () => {
if (isDefined(timeoutId.current)) {
clearTimeout(timeoutId.current);
}
};
}
}, [delay, startTimeout]);
const pauseTimeout = () => {
if (isDefined(timeoutId.current)) {
clearTimeout(timeoutId.current);
}
const elapsedTime = Date.now() - startTime.current;
remainingTime.current = remainingTime.current - elapsedTime;
};
const resumeTimeout = () => {
startTimeout();
};
return { pauseTimeout, resumeTimeout };
};

View File

@ -1,4 +1,5 @@
import { MAIN_COLORS } from '@/ui/theme/constants/MainColors';
import { MAIN_COLORS } from 'twenty-ui';
import { SECONDARY_COLORS } from '@/ui/theme/constants/SecondaryColors';
export const COLOR = {

View File

@ -1,4 +1,4 @@
import { MAIN_COLORS } from '@/ui/theme/constants/MainColors';
import { MAIN_COLORS } from 'twenty-ui';
export const MAIN_COLOR_NAMES = Object.keys(MAIN_COLORS) as ThemeColor[];

View File

@ -1,15 +0,0 @@
/* eslint-disable @nx/workspace-no-hardcoded-colors */
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
export const MAIN_COLORS = {
green: '#55ef3c',
turquoise: '#15de8f',
sky: '#00e0ff',
blue: '#1961ed',
purple: '#915ffd',
pink: '#f54bd0',
red: '#f83e3e',
orange: '#ff7222',
yellow: '#ffd338',
gray: GRAY_SCALE.gray30,
};

View File

@ -1,6 +1,5 @@
import { css } from '@emotion/react';
import { ThemeType } from './ThemeLight';
import { ThemeType } from 'twenty-ui';
export const OVERLAY_BACKGROUND = (props: { theme: ThemeType }) => css`
backdrop-filter: blur(12px) saturate(200%) contrast(50%) brightness(130%);

View File

@ -1,6 +1,5 @@
import { css } from '@emotion/react';
import { ThemeType } from './ThemeLight';
import { ThemeType } from 'twenty-ui';
export const TEXT_INPUT_STYLE = (props: { theme: ThemeType }) => css`
background-color: transparent;

View File

@ -1,21 +0,0 @@
import { ACCENT_DARK } from '@/ui/theme/constants/AccentDark';
import { BACKGROUND_DARK } from '@/ui/theme/constants/BackgroundDark';
import { BORDER_DARK } from '@/ui/theme/constants/BorderDark';
import { BOX_SHADOW_DARK } from '@/ui/theme/constants/BoxShadowDark';
import { FONT_DARK } from '@/ui/theme/constants/FontDark';
import { TAG_DARK } from '@/ui/theme/constants/TagDark';
import { THEME_COMMON } from '@/ui/theme/constants/ThemeCommon';
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
export const THEME_DARK: ThemeType = {
...THEME_COMMON,
...{
accent: ACCENT_DARK,
background: BACKGROUND_DARK,
border: BORDER_DARK,
tag: TAG_DARK,
boxShadow: BOX_SHADOW_DARK,
font: FONT_DARK,
name: 'dark',
},
};

View File

@ -1,22 +0,0 @@
import { ACCENT_LIGHT } from '@/ui/theme/constants/AccentLight';
import { BACKGROUND_LIGHT } from '@/ui/theme/constants/BackgroundLight';
import { BORDER_LIGHT } from '@/ui/theme/constants/BorderLight';
import { BOX_SHADOW_LIGHT } from '@/ui/theme/constants/BoxShadowLight';
import { FONT_LIGHT } from '@/ui/theme/constants/FontLight';
import { TAG_LIGHT } from '@/ui/theme/constants/TagLight';
import { THEME_COMMON } from '@/ui/theme/constants/ThemeCommon';
export const THEME_LIGHT = {
...THEME_COMMON,
...{
accent: ACCENT_LIGHT,
background: BACKGROUND_LIGHT,
border: BORDER_LIGHT,
tag: TAG_LIGHT,
boxShadow: BOX_SHADOW_LIGHT,
font: FONT_LIGHT,
name: 'light',
},
};
export type ThemeType = typeof THEME_LIGHT;