Settings Option Card component (#8456)

fixes - #8195

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
nitin
2024-11-18 14:52:33 +05:30
committed by GitHub
parent ade1c57ff4
commit 2f5dc26545
56 changed files with 931 additions and 920 deletions

View File

@ -0,0 +1,68 @@
import { useExpandedAnimation } from '@/settings/hooks/useExpandedAnimation';
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
import styled from '@emotion/styled';
import { AnimatePresence, motion } from 'framer-motion';
import { useRecoilValue } from 'recoil';
import { IconTool, MAIN_COLORS } from 'twenty-ui';
const StyledAdvancedWrapper = styled.div`
position: relative;
width: 100%;
`;
const StyledIconContainer = styled.div`
border-right: 1px solid ${MAIN_COLORS.yellow};
display: flex;
height: 100%;
left: ${({ theme }) => theme.spacing(-6)};
position: absolute;
top: 0;
`;
const StyledContent = styled.div`
width: 100%;
`;
const StyledIconTool = styled(IconTool)`
margin-right: ${({ theme }) => theme.spacing(0.5)};
`;
type AdvancedSettingsWrapperProps = {
children: React.ReactNode;
dimension?: 'width' | 'height';
hideIcon?: boolean;
};
export const AdvancedSettingsWrapper = ({
children,
dimension = 'height',
hideIcon = false,
}: AdvancedSettingsWrapperProps) => {
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
const { contentRef, motionAnimationVariants } = useExpandedAnimation(
isAdvancedModeEnabled,
dimension,
);
return (
<AnimatePresence>
{isAdvancedModeEnabled && (
<motion.div
ref={contentRef}
initial="initial"
animate="animate"
exit="exit"
variants={motionAnimationVariants}
>
<StyledAdvancedWrapper>
{!hideIcon && (
<StyledIconContainer>
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
</StyledIconContainer>
)}
<StyledContent>{children}</StyledContent>
</StyledAdvancedWrapper>
</motion.div>
)}
</AnimatePresence>
);
};

View File

@ -0,0 +1,103 @@
import { TextInput } from '@/ui/input/components/TextInput';
import styled from '@emotion/styled';
import { Button, IconMinus, IconPlus } from 'twenty-ui';
import { castAsNumberOrNull } from '~/utils/cast-as-number-or-null';
type SettingsCounterProps = {
value: number;
onChange: (value: number) => void;
minValue?: number;
maxValue?: number;
disabled?: boolean;
};
const StyledCounterContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
margin-left: auto;
width: ${({ theme }) => theme.spacing(30)};
`;
const StyledTextInput = styled(TextInput)`
width: ${({ theme }) => theme.spacing(16)};
input {
width: ${({ theme }) => theme.spacing(16)};
height: ${({ theme }) => theme.spacing(6)};
text-align: center;
font-weight: ${({ theme }) => theme.font.weight.medium};
}
`;
const StyledControlButton = styled(Button)`
height: ${({ theme }) => theme.spacing(6)};
width: ${({ theme }) => theme.spacing(6)};
padding: 0;
justify-content: center;
svg {
height: ${({ theme }) => theme.spacing(4)};
width: ${({ theme }) => theme.spacing(4)};
}
`;
export const SettingsCounter = ({
value,
onChange,
minValue = 0,
maxValue = 100,
disabled = false,
}: SettingsCounterProps) => {
const handleIncrementCounter = () => {
if (value < maxValue) {
onChange(value + 1);
}
};
const handleDecrementCounter = () => {
if (value > minValue) {
onChange(value - 1);
}
};
const handleTextInputChange = (value: string) => {
const castedNumber = castAsNumberOrNull(value);
if (castedNumber === null) {
onChange(minValue);
return;
}
if (castedNumber < minValue) {
return;
}
if (castedNumber > maxValue) {
onChange(maxValue);
return;
}
onChange(castedNumber);
};
return (
<StyledCounterContainer>
<StyledControlButton
variant="secondary"
onClick={handleDecrementCounter}
Icon={IconMinus}
disabled={disabled}
/>
<StyledTextInput
name="counter"
fullWidth
value={value.toString()}
onChange={handleTextInputChange}
disabled={disabled}
/>
<StyledControlButton
variant="secondary"
onClick={handleIncrementCounter}
Icon={IconPlus}
disabled={disabled}
/>
</StyledCounterContainer>
);
};

View File

@ -23,7 +23,7 @@ import {
import { useAuth } from '@/auth/hooks/useAuth';
import { billingState } from '@/client-config/states/billingState';
import { SettingsNavigationDrawerItem } from '@/settings/components/SettingsNavigationDrawerItem';
import { useExpandedHeightAnimation } from '@/settings/hooks/useExpandedHeightAnimation';
import { useExpandedAnimation } from '@/settings/hooks/useExpandedAnimation';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import {
@ -69,7 +69,7 @@ const StyledIconTool = styled(IconTool)`
export const SettingsNavigationDrawerItems = () => {
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
const { contentRef, motionAnimationVariants } = useExpandedHeightAnimation(
const { contentRef, motionAnimationVariants } = useExpandedAnimation(
isAdvancedModeEnabled,
);
const { signOut } = useAuth();

View File

@ -1,95 +0,0 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useId } from 'react';
import { CardContent, IconComponent, Toggle } from 'twenty-ui';
type SettingsOptionCardContentProps = {
Icon?: IconComponent;
title: React.ReactNode;
description: string;
divider?: boolean;
checked: boolean;
onChange: (checked: boolean) => void;
};
const StyledCardContent = styled(CardContent)`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(4)};
cursor: pointer;
position: relative;
&:hover {
background: ${({ theme }) => theme.background.transparent.lighter};
}
`;
const StyledTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledDescription = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
`;
const StyledIcon = styled.div`
align-items: center;
border: 2px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
background-color: ${({ theme }) => theme.background.primary};
display: flex;
height: ${({ theme }) => theme.spacing(8)};
justify-content: center;
width: ${({ theme }) => theme.spacing(8)};
min-width: ${({ theme }) => theme.icon.size.md};
`;
const StyledToggle = styled(Toggle)`
margin-left: auto;
`;
const StyledCover = styled.span`
cursor: pointer;
inset: 0;
position: absolute;
`;
export const SettingsOptionCardContent = ({
Icon,
title,
description,
divider,
checked,
onChange,
}: SettingsOptionCardContentProps) => {
const theme = useTheme();
const toggleId = useId();
return (
<StyledCardContent divider={divider}>
{Icon && (
<StyledIcon>
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.md} />
</StyledIcon>
)}
<div>
<StyledTitle>
<label htmlFor={toggleId}>
{title}
<StyledCover />
</label>
</StyledTitle>
<StyledDescription>{description}</StyledDescription>
</div>
<StyledToggle id={toggleId} value={checked} onChange={onChange} />
</StyledCardContent>
);
};

View File

@ -0,0 +1,37 @@
import styled from '@emotion/styled';
import { CardContent } from 'twenty-ui';
type StyledCardContentProps = {
disabled?: boolean;
divider?: boolean;
};
export const StyledSettingsOptionCardContent = styled(
CardContent,
)<StyledCardContentProps>`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(4)};
`;
export const StyledSettingsOptionCardIcon = styled.div`
align-items: center;
border: 2px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
background-color: ${({ theme }) => theme.background.primary};
display: flex;
height: ${({ theme }) => theme.spacing(8)};
justify-content: center;
width: ${({ theme }) => theme.spacing(8)};
min-width: ${({ theme }) => theme.icon.size.md};
`;
export const StyledSettingsOptionCardTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-bottom: ${({ theme }) => theme.spacing(1)};
`;
export const StyledSettingsOptionCardDescription = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
`;

View File

@ -0,0 +1,57 @@
import { SettingsCounter } from '@/settings/components/SettingsCounter';
import {
StyledSettingsOptionCardContent,
StyledSettingsOptionCardDescription,
StyledSettingsOptionCardIcon,
StyledSettingsOptionCardTitle,
} from '@/settings/components/SettingsOptions/SettingsOptionCardContentBase';
import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer';
import { IconComponent } from 'twenty-ui';
type SettingsOptionCardContentCounterProps = {
Icon?: IconComponent;
title: React.ReactNode;
description?: string;
divider?: boolean;
disabled?: boolean;
value: number;
onChange: (value: number) => void;
minValue?: number;
maxValue?: number;
};
export const SettingsOptionCardContentCounter = ({
Icon,
title,
description,
divider,
disabled = false,
value,
onChange,
minValue,
maxValue,
}: SettingsOptionCardContentCounterProps) => {
return (
<StyledSettingsOptionCardContent divider={divider} disabled={disabled}>
{Icon && (
<StyledSettingsOptionCardIcon>
<SettingsOptionIconCustomizer Icon={Icon} />
</StyledSettingsOptionCardIcon>
)}
<div>
<StyledSettingsOptionCardTitle>{title}</StyledSettingsOptionCardTitle>
{description && (
<StyledSettingsOptionCardDescription>
{description}
</StyledSettingsOptionCardDescription>
)}
</div>
<SettingsCounter
value={value}
onChange={onChange}
minValue={minValue}
maxValue={maxValue}
/>
</StyledSettingsOptionCardContent>
);
};

View File

@ -0,0 +1,75 @@
import {
StyledSettingsOptionCardContent,
StyledSettingsOptionCardDescription,
StyledSettingsOptionCardIcon,
StyledSettingsOptionCardTitle,
} from '@/settings/components/SettingsOptions/SettingsOptionCardContentBase';
import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer';
import { Select } from '@/ui/input/components/Select';
import styled from '@emotion/styled';
import { IconComponent } from 'twenty-ui';
const StyledSettingsOptionCardSelect = styled(Select)`
margin-left: auto;
width: 120px;
`;
type SelectValue = string | number | boolean | null;
type SettingsOptionCardContentSelectProps<Value extends SelectValue> = {
Icon?: IconComponent;
title: React.ReactNode;
description?: string;
divider?: boolean;
disabled?: boolean;
value: Value;
onChange: (value: SelectValue) => void;
options: {
value: Value;
label: string;
Icon?: IconComponent;
}[];
selectClassName?: string;
dropdownId: string;
fullWidth?: boolean;
};
export const SettingsOptionCardContentSelect = <Value extends SelectValue>({
Icon,
title,
description,
divider,
disabled = false,
value,
onChange,
options,
selectClassName,
dropdownId,
fullWidth,
}: SettingsOptionCardContentSelectProps<Value>) => {
return (
<StyledSettingsOptionCardContent divider={divider} disabled={disabled}>
{Icon && (
<StyledSettingsOptionCardIcon>
<SettingsOptionIconCustomizer Icon={Icon} />
</StyledSettingsOptionCardIcon>
)}
<div>
<StyledSettingsOptionCardTitle>{title}</StyledSettingsOptionCardTitle>
<StyledSettingsOptionCardDescription>
{description}
</StyledSettingsOptionCardDescription>
</div>
<StyledSettingsOptionCardSelect
className={selectClassName}
dropdownWidth={fullWidth ? 'auto' : 120}
disabled={disabled}
dropdownId={dropdownId}
value={value}
onChange={onChange}
options={options}
selectSizeVariant="small"
/>
</StyledSettingsOptionCardContent>
);
};

View File

@ -0,0 +1,89 @@
import {
StyledSettingsOptionCardContent,
StyledSettingsOptionCardDescription,
StyledSettingsOptionCardIcon,
StyledSettingsOptionCardTitle,
} from '@/settings/components/SettingsOptions/SettingsOptionCardContentBase';
import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useId } from 'react';
import { IconComponent, Toggle } from 'twenty-ui';
const StyledSettingsOptionCardToggleContent = styled(
StyledSettingsOptionCardContent,
)`
cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')};
position: relative;
pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')};
&:hover {
background: ${({ theme }) => theme.background.transparent.lighter};
}
`;
const StyledSettingsOptionCardToggleButton = styled(Toggle)`
margin-left: auto;
`;
const StyledSettingsOptionCardToggleCover = styled.span`
cursor: pointer;
inset: 0;
position: absolute;
`;
type SettingsOptionCardContentToggleProps = {
Icon?: IconComponent;
title: React.ReactNode;
description?: string;
divider?: boolean;
disabled?: boolean;
advancedMode?: boolean;
checked: boolean;
onChange: (checked: boolean) => void;
};
export const SettingsOptionCardContentToggle = ({
Icon,
title,
description,
divider,
disabled = false,
advancedMode = false,
checked,
onChange,
}: SettingsOptionCardContentToggleProps) => {
const theme = useTheme();
const toggleId = useId();
return (
<StyledSettingsOptionCardToggleContent
divider={divider}
disabled={disabled}
>
{Icon && (
<StyledSettingsOptionCardIcon>
<SettingsOptionIconCustomizer Icon={Icon} />
</StyledSettingsOptionCardIcon>
)}
<div>
<StyledSettingsOptionCardTitle>
<label htmlFor={toggleId}>
{title}
<StyledSettingsOptionCardToggleCover />
</label>
</StyledSettingsOptionCardTitle>
<StyledSettingsOptionCardDescription>
{description}
</StyledSettingsOptionCardDescription>
</div>
<StyledSettingsOptionCardToggleButton
id={toggleId}
value={checked}
onChange={onChange}
disabled={disabled}
color={advancedMode ? theme.color.yellow : theme.color.blue}
/>
</StyledSettingsOptionCardToggleContent>
);
};

View File

@ -0,0 +1,33 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from 'twenty-ui';
type SettingsOptionIconCustomizerProps = {
Icon: IconComponent;
zoom?: number;
rotate?: number;
};
const StyledIconCustomizer = styled.div<{ zoom: number; rotate: number }>`
display: inline-flex;
align-items: center;
justify-content: center;
transform: scale(${({ zoom }) => zoom}) rotate(${({ rotate }) => rotate}deg);
`;
export const SettingsOptionIconCustomizer = ({
Icon,
zoom = 1,
rotate = -4,
}: SettingsOptionIconCustomizerProps) => {
const theme = useTheme();
return (
<StyledIconCustomizer zoom={zoom} rotate={rotate}>
<Icon
size={theme.icon.size.xl}
color={theme.IllustrationIcon.color.grey}
stroke={theme.icon.stroke.md}
/>
</StyledIconCustomizer>
);
};