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:
@ -29,9 +29,8 @@ initialize({
|
||||
const preview: Preview = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const mode = useDarkMode() ? 'Dark' : 'Light';
|
||||
const theme = useDarkMode() ? THEME_DARK : THEME_LIGHT;
|
||||
|
||||
const theme = mode === 'Dark' ? THEME_DARK : THEME_LIGHT;
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Story />
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
|
||||
type CustomResolverQueryResult<
|
||||
@ -62,7 +63,7 @@ export const useCustomResolver = <
|
||||
variables: queryVariables,
|
||||
onError: (error) => {
|
||||
enqueueSnackBar(error.message || `Error loading ${objectName}`, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
|
||||
|
||||
@ -12,7 +13,7 @@ export const useHandleResetPassword = () => {
|
||||
return async () => {
|
||||
if (!email) {
|
||||
enqueueSnackBar('Invalid email', {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -24,16 +25,16 @@ export const useHandleResetPassword = () => {
|
||||
|
||||
if (data?.emailPasswordResetLink?.success === true) {
|
||||
enqueueSnackBar('Password reset link has been sent to the email', {
|
||||
variant: 'success',
|
||||
variant: SnackBarVariant.Success,
|
||||
});
|
||||
} else {
|
||||
enqueueSnackBar('There was some issue', {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -7,6 +7,7 @@ import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
|
||||
@ -75,7 +76,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueSnackBar(`${error.message}`, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
@ -124,7 +125,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
navigateAfterSignInUp(currentWorkspace, currentWorkspaceMember);
|
||||
} catch (err: any) {
|
||||
enqueueSnackBar(err?.message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
|
||||
export const PromiseRejectionEffect = () => {
|
||||
@ -15,12 +16,12 @@ export const PromiseRejectionEffect = () => {
|
||||
enqueueSnackBar(
|
||||
`Error with custom object that cannot be found : ${event.reason}`,
|
||||
{
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
enqueueSnackBar(`Error: ${event.reason}`, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import {
|
||||
FieldFilter,
|
||||
@ -43,7 +44,7 @@ export const useFindManyObjectMetadataItems = ({
|
||||
enqueueSnackBar(
|
||||
`Error during useFindManyObjectMetadataItems, ${error.message}`,
|
||||
{
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@ -9,6 +9,7 @@ import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/
|
||||
import { useFindDuplicateRecordsQuery } from '@/object-record/hooks/useFindDuplicatesRecordsQuery';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/utils/getFindDuplicateRecordsQueryResponseField';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { logError } from '~/utils/logError';
|
||||
|
||||
@ -54,7 +55,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
enqueueSnackBar(
|
||||
`Error during useFindDuplicateRecords for "${objectMetadataItem.nameSingular}", ${error.message}`,
|
||||
{
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@ -17,6 +17,7 @@ import { RecordGqlOperationVariables } from '@/object-record/graphql/types/Recor
|
||||
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { logError } from '~/utils/logError';
|
||||
@ -116,7 +117,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
enqueueSnackBar(
|
||||
`Error during useFindManyRecords for "${objectMetadataItem.namePlural}", ${error.message}`,
|
||||
{
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -192,7 +193,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
enqueueSnackBar(
|
||||
`Error during fetchMoreObjects for "${objectMetadataItem.namePlural}", ${error}`,
|
||||
{
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
|
||||
@ -2,6 +2,7 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconCopy } from 'twenty-ui';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
|
||||
@ -24,7 +25,7 @@ export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => {
|
||||
Icon={IconCopy}
|
||||
onClick={() => {
|
||||
enqueueSnackBar('Text copied to clipboard', {
|
||||
variant: 'success',
|
||||
variant: SnackBarVariant.Success,
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@ import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords
|
||||
import { getSpreadSheetValidation } from '@/object-record/spreadsheet-import/util/getSpreadSheetValidation';
|
||||
import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport';
|
||||
import { SpreadsheetOptions, Validation } from '@/spreadsheet-import/types';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -163,7 +164,7 @@ export const useSpreadsheetRecordImport = (objectNameSingular: string) => {
|
||||
await createManyRecords(createInputs);
|
||||
} catch (error: any) {
|
||||
enqueueSnackBar(error?.message || 'Something went wrong', {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -2,6 +2,7 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconCopy } from 'twenty-ui';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
@ -32,7 +33,7 @@ export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => {
|
||||
title="Copy"
|
||||
onClick={() => {
|
||||
enqueueSnackBar('Api Key copied to clipboard', {
|
||||
variant: 'success',
|
||||
variant: SnackBarVariant.Success,
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
@ -20,6 +20,7 @@ import { getConnectionDbName } from '@/settings/integrations/utils/getConnection
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { Info } from '@/ui/display/info/components/Info';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||
import {
|
||||
@ -91,7 +92,7 @@ export const SettingsIntegrationEditDatabaseConnectionContent = ({
|
||||
);
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
|
||||
import { H2Title } from 'twenty-ui';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
|
||||
@ -16,7 +17,7 @@ export const ChangePassword = () => {
|
||||
const handlePasswordResetClick = async () => {
|
||||
if (!currentUser?.email) {
|
||||
enqueueSnackBar('Invalid email', {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -29,16 +30,16 @@ export const ChangePassword = () => {
|
||||
});
|
||||
if (data?.emailPasswordResetLink?.success === true) {
|
||||
enqueueSnackBar('Password reset link has been sent to the email', {
|
||||
variant: 'success',
|
||||
variant: SnackBarVariant.Success,
|
||||
});
|
||||
} else {
|
||||
enqueueSnackBar('There was some issue', {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Toggle } from '@/ui/input/components/Toggle';
|
||||
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
|
||||
@ -32,7 +33,7 @@ export const ToggleImpersonate = () => {
|
||||
});
|
||||
} catch (err: any) {
|
||||
enqueueSnackBar(err?.message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -12,6 +12,7 @@ import { setColumn } from '@/spreadsheet-import/utils/setColumn';
|
||||
import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
|
||||
import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn';
|
||||
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
|
||||
@ -170,7 +171,7 @@ export const MatchColumnsStep = <T extends string>({
|
||||
} else if (index === existingFieldIndex) {
|
||||
enqueueSnackBar('Columns cannot duplicate', {
|
||||
title: 'Another column unselected',
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
return setColumn(column);
|
||||
} else {
|
||||
|
||||
@ -8,6 +8,7 @@ import { RawData } from '@/spreadsheet-import/types';
|
||||
import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords';
|
||||
import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook';
|
||||
import { CircularProgressBar } from '@/ui/feedback/progress-bar/components/CircularProgressBar';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
|
||||
@ -80,7 +81,7 @@ export const UploadFlow = ({ nextStep }: UploadFlowProps) => {
|
||||
(description: string) => {
|
||||
enqueueSnackBar(description, {
|
||||
title: 'Error',
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
[enqueueSnackBar],
|
||||
|
||||
@ -5,6 +5,7 @@ import * as XLSX from 'xlsx-ugnis';
|
||||
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
|
||||
@ -114,7 +115,7 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
||||
fileRejections.forEach((fileRejection) => {
|
||||
enqueueSnackBar(fileRejection.errors[0].message, {
|
||||
title: `${fileRejection.file.name} upload rejected`,
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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 }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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 };
|
||||
};
|
||||
@ -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 = {
|
||||
|
||||
@ -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[];
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -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%);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
},
|
||||
};
|
||||
@ -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;
|
||||
@ -2,6 +2,7 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconCopy, IconLink } from 'twenty-ui';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
@ -40,7 +41,7 @@ export const WorkspaceInviteLink = ({
|
||||
title="Copy link"
|
||||
onClick={() => {
|
||||
enqueueSnackBar('Link copied to clipboard', {
|
||||
variant: 'success',
|
||||
variant: SnackBarVariant.Success,
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
@ -11,6 +11,7 @@ import { SubscriptionCard } from '@/billing/components/SubscriptionCard';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { CardPicker } from '@/ui/input/components/CardPicker';
|
||||
@ -131,7 +132,7 @@ export const ChooseYourPlan = () => {
|
||||
enqueueSnackBar(
|
||||
'Checkout session error. Please retry or contact Twenty team',
|
||||
{
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
return;
|
||||
|
||||
@ -16,6 +16,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
@ -114,7 +115,7 @@ export const CreateProfile = () => {
|
||||
);
|
||||
} catch (error: any) {
|
||||
enqueueSnackBar(error?.message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -18,6 +18,7 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetada
|
||||
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
@ -93,7 +94,7 @@ export const CreateWorkspace = () => {
|
||||
}, 20);
|
||||
} catch (error: any) {
|
||||
enqueueSnackBar(error?.message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -12,6 +12,7 @@ import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspace
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching';
|
||||
@ -61,7 +62,7 @@ export const Invite = () => {
|
||||
!workspaceFromInviteHashLoading
|
||||
) {
|
||||
enqueueSnackBar('workspace does not exist', {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
if (isDefined(currentWorkspace)) {
|
||||
navigate(AppPath.Index);
|
||||
@ -76,7 +77,7 @@ export const Invite = () => {
|
||||
enqueueSnackBar(
|
||||
`You already belong to ${workspaceFromInviteHash?.displayName} workspace`,
|
||||
{
|
||||
variant: 'info',
|
||||
variant: SnackBarVariant.Info,
|
||||
},
|
||||
);
|
||||
navigate(AppPath.Index);
|
||||
|
||||
@ -16,6 +16,7 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { useNavigateAfterSignInUp } from '@/auth/sign-in-up/hooks/useNavigateAfterSignInUp';
|
||||
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
@ -95,7 +96,7 @@ export const PasswordReset = () => {
|
||||
skip: !passwordResetToken,
|
||||
onError: (error) => {
|
||||
enqueueSnackBar(error?.message ?? 'Token Invalid', {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
if (!isLoggedIn) {
|
||||
navigate(AppPath.SignInUp);
|
||||
@ -128,14 +129,14 @@ export const PasswordReset = () => {
|
||||
|
||||
if (!data?.updatePasswordViaResetToken.success) {
|
||||
enqueueSnackBar('There was an error while updating password.', {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
enqueueSnackBar('Password has been updated', {
|
||||
variant: 'success',
|
||||
variant: SnackBarVariant.Success,
|
||||
});
|
||||
navigate(AppPath.Index);
|
||||
return;
|
||||
@ -152,7 +153,7 @@ export const PasswordReset = () => {
|
||||
enqueueSnackBar(
|
||||
(err as Error)?.message || 'An error occurred while updating password',
|
||||
{
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
|
||||
import { SupportChat } from '@/support/components/SupportChat';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { Info } from '@/ui/display/info/components/Info';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
@ -127,13 +128,13 @@ export const SettingsBilling = () => {
|
||||
setCurrentWorkspace(newCurrentWorkspace);
|
||||
}
|
||||
enqueueSnackBar(`Subscription has been switched ${switchingInfo.to}`, {
|
||||
variant: 'success',
|
||||
variant: SnackBarVariant.Success,
|
||||
});
|
||||
} catch (error: any) {
|
||||
enqueueSnackBar(
|
||||
`Error while switching subscription ${switchingInfo.to}.`,
|
||||
{
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
import { settingsCreateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsCreateObjectInputSchema';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
@ -58,7 +59,7 @@ export const SettingsNewObject = () => {
|
||||
);
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -22,6 +22,7 @@ import { settingsUpdateObjectInputSchema } from '@/settings/data-model/validatio
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
@ -83,7 +84,7 @@ export const SettingsObjectEdit = () => {
|
||||
navigate(`${settingsObjectsPagePath}/${getObjectSlug(formValues)}`);
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -24,6 +24,7 @@ import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fi
|
||||
import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
|
||||
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
@ -136,7 +137,7 @@ export const SettingsObjectFieldEdit = () => {
|
||||
navigate(`/settings/objects/${objectSlug}`);
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -25,6 +25,7 @@ import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/f
|
||||
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
|
||||
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
@ -262,7 +263,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
navigate(`/settings/objects/${objectSlug}`);
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -20,6 +20,7 @@ import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
@ -123,7 +124,7 @@ export const SettingsIntegrationNewDatabaseConnection = () => {
|
||||
);
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: 'error',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
||||
import { ThemeType } from 'twenty-ui';
|
||||
|
||||
export { ThemeProvider } from '@emotion/react';
|
||||
|
||||
export { THEME_DARK } from './src/modules/ui/theme/constants/ThemeDark';
|
||||
export { THEME_LIGHT } from './src/modules/ui/theme/constants/ThemeLight';
|
||||
|
||||
declare module '@emotion/react' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Theme extends ThemeType {}
|
||||
|
||||
@ -133,6 +133,7 @@ export {
|
||||
IconSend,
|
||||
IconSettings,
|
||||
IconSortDescending,
|
||||
IconSquareRoundedCheck,
|
||||
IconTable,
|
||||
IconTag,
|
||||
IconTags,
|
||||
|
||||
21
packages/twenty-ui/src/theme/constants/SnackBarCommon.ts
Normal file
21
packages/twenty-ui/src/theme/constants/SnackBarCommon.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { MAIN_COLORS } from '@ui/theme/constants/MainColors';
|
||||
import { RGBA } from '@ui/theme/constants/Rgba';
|
||||
|
||||
export const SNACK_BAR_COMMON = {
|
||||
success: {
|
||||
color: MAIN_COLORS.turquoise,
|
||||
backgroundColor: RGBA(MAIN_COLORS.turquoise, 0.04),
|
||||
},
|
||||
error: {
|
||||
color: MAIN_COLORS.red,
|
||||
backgroundColor: RGBA(MAIN_COLORS.red, 0.04),
|
||||
},
|
||||
warning: {
|
||||
color: MAIN_COLORS.orange,
|
||||
backgroundColor: RGBA(MAIN_COLORS.orange, 0.04),
|
||||
},
|
||||
info: {
|
||||
color: MAIN_COLORS.blue,
|
||||
backgroundColor: RGBA(MAIN_COLORS.blue, 0.04),
|
||||
},
|
||||
};
|
||||
11
packages/twenty-ui/src/theme/constants/SnackBarDark.ts
Normal file
11
packages/twenty-ui/src/theme/constants/SnackBarDark.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { BACKGROUND_DARK } from '@ui/theme/constants/BackgroundDark';
|
||||
import { FONT_DARK } from '@ui/theme/constants/FontDark';
|
||||
import { SNACK_BAR_COMMON } from '@ui/theme/constants/SnackBarCommon';
|
||||
|
||||
export const SNACK_BAR_DARK = {
|
||||
...SNACK_BAR_COMMON,
|
||||
default: {
|
||||
color: FONT_DARK.color.primary,
|
||||
backgroundColor: BACKGROUND_DARK.transparent.light,
|
||||
},
|
||||
};
|
||||
11
packages/twenty-ui/src/theme/constants/SnackBarLight.ts
Normal file
11
packages/twenty-ui/src/theme/constants/SnackBarLight.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { BACKGROUND_LIGHT } from '@ui/theme/constants/BackgroundLight';
|
||||
import { FONT_LIGHT } from '@ui/theme/constants/FontLight';
|
||||
import { SNACK_BAR_COMMON } from '@ui/theme/constants/SnackBarCommon';
|
||||
|
||||
export const SNACK_BAR_LIGHT = {
|
||||
...SNACK_BAR_COMMON,
|
||||
default: {
|
||||
color: FONT_LIGHT.color.primary,
|
||||
backgroundColor: BACKGROUND_LIGHT.transparent.light,
|
||||
},
|
||||
};
|
||||
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import { ANIMATION } from './Animation';
|
||||
import { BLUR } from './Blur';
|
||||
import { COLOR } from './Colors';
|
||||
@ -15,20 +14,6 @@ export const THEME_COMMON = {
|
||||
text: TEXT,
|
||||
blur: BLUR,
|
||||
animation: ANIMATION,
|
||||
snackBar: {
|
||||
success: {
|
||||
background: '#16A26B',
|
||||
color: '#D0F8E9',
|
||||
},
|
||||
error: {
|
||||
background: '#B43232',
|
||||
color: '#FED8D8',
|
||||
},
|
||||
info: {
|
||||
background: COLOR.gray80,
|
||||
color: GRAY_SCALE.gray0,
|
||||
},
|
||||
},
|
||||
spacingMultiplicator: 4,
|
||||
spacing: (...args: number[]) =>
|
||||
args.map((multiplicator) => `${multiplicator * 4}px`).join(' '),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ThemeType } from '..';
|
||||
import { SNACK_BAR_DARK, ThemeType } from '..';
|
||||
|
||||
import { ACCENT_DARK } from './AccentDark';
|
||||
import { BACKGROUND_DARK } from './BackgroundDark';
|
||||
@ -14,9 +14,10 @@ export const THEME_DARK: ThemeType = {
|
||||
accent: ACCENT_DARK,
|
||||
background: BACKGROUND_DARK,
|
||||
border: BORDER_DARK,
|
||||
tag: TAG_DARK,
|
||||
boxShadow: BOX_SHADOW_DARK,
|
||||
font: FONT_DARK,
|
||||
name: 'dark',
|
||||
snackBar: SNACK_BAR_DARK,
|
||||
tag: TAG_DARK,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { SNACK_BAR_LIGHT } from '@ui/theme/constants/SnackBarLight';
|
||||
|
||||
import { ACCENT_LIGHT } from './AccentLight';
|
||||
import { BACKGROUND_LIGHT } from './BackgroundLight';
|
||||
import { BORDER_LIGHT } from './BorderLight';
|
||||
@ -12,9 +14,10 @@ export const THEME_LIGHT = {
|
||||
accent: ACCENT_LIGHT,
|
||||
background: BACKGROUND_LIGHT,
|
||||
border: BORDER_LIGHT,
|
||||
tag: TAG_LIGHT,
|
||||
boxShadow: BOX_SHADOW_LIGHT,
|
||||
font: FONT_LIGHT,
|
||||
name: 'light',
|
||||
snackBar: SNACK_BAR_LIGHT,
|
||||
tag: TAG_LIGHT,
|
||||
},
|
||||
};
|
||||
|
||||
@ -23,6 +23,9 @@ export * from './constants/Modal';
|
||||
export * from './constants/OverlayBackground';
|
||||
export * from './constants/Rgba';
|
||||
export * from './constants/SecondaryColors';
|
||||
export * from './constants/SnackBarCommon';
|
||||
export * from './constants/SnackBarDark';
|
||||
export * from './constants/SnackBarLight';
|
||||
export * from './constants/TagDark';
|
||||
export * from './constants/TagLight';
|
||||
export * from './constants/Text';
|
||||
|
||||
Reference in New Issue
Block a user