Settings Option Card component (#8456)
fixes - #8195 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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};
|
||||
`;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user