Feat/performance-refactor-styled-component (#5516)
In this PR I'm optimizing a whole RecordTableCell in real conditions with a complex RelationFieldDisplay component : - Broke down getObjectRecordIdentifier into multiple utils - Precompute memoized function for getting chip data per field with useRecordChipDataGenerator() - Refactored RelationFieldDisplay - Use CSS modules where performance is needed instead of styled components - Create a CSS theme with global CSS variables to be used by CSS modules
This commit is contained in:
@ -0,0 +1,23 @@
|
||||
.avatar {
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-on-click:hover {
|
||||
box-shadow: 0 0 0 4px var(--twentycrm-background-transparent-light);
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@ -1,9 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { isNonEmptyString, isUndefined } from '@sniptt/guards';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { Nullable, stringToHslColor } from '@ui/utilities';
|
||||
|
||||
import styles from './Avatar.module.css';
|
||||
|
||||
export type AvatarType = 'squared' | 'rounded';
|
||||
|
||||
export type AvatarSize = 'xl' | 'lg' | 'md' | 'sm' | 'xs';
|
||||
@ -43,37 +45,8 @@ const propertiesBySize = {
|
||||
},
|
||||
};
|
||||
|
||||
export const StyledAvatar = styled.div<
|
||||
AvatarProps & { color: string; backgroundColor: string }
|
||||
>`
|
||||
align-items: center;
|
||||
background-color: ${({ backgroundColor }) => backgroundColor};
|
||||
${({ avatarUrl }) =>
|
||||
isNonEmptyString(avatarUrl) ? `background-image: url(${avatarUrl});` : ''}
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: ${(props) => (props.type === 'rounded' ? '50%' : '2px')};
|
||||
color: ${({ color }) => color};
|
||||
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
|
||||
display: flex;
|
||||
|
||||
flex-shrink: 0;
|
||||
font-size: ${({ size = 'md' }) => propertiesBySize[size].fontSize};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
|
||||
height: ${({ size = 'md' }) => propertiesBySize[size].width};
|
||||
justify-content: center;
|
||||
width: ${({ size = 'md' }) => propertiesBySize[size].width};
|
||||
|
||||
&:hover {
|
||||
box-shadow: ${({ theme, onClick }) =>
|
||||
onClick ? '0 0 0 4px ' + theme.background.transparent.light : 'unset'};
|
||||
}
|
||||
`;
|
||||
|
||||
export const Avatar = ({
|
||||
avatarUrl,
|
||||
className,
|
||||
size = 'md',
|
||||
placeholder,
|
||||
entityId = placeholder,
|
||||
@ -82,42 +55,50 @@ export const Avatar = ({
|
||||
color,
|
||||
backgroundColor,
|
||||
}: AvatarProps) => {
|
||||
const noAvatarUrl = !isNonEmptyString(avatarUrl);
|
||||
const [isInvalidAvatarUrl, setIsInvalidAvatarUrl] = useState(false);
|
||||
useEffect(() => {
|
||||
if (isNonEmptyString(avatarUrl)) {
|
||||
new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(false);
|
||||
img.onerror = () => resolve(true);
|
||||
img.src = avatarUrl;
|
||||
}).then((res) => {
|
||||
setIsInvalidAvatarUrl(res as boolean);
|
||||
});
|
||||
}
|
||||
}, [avatarUrl]);
|
||||
|
||||
const noAvatarUrl = !isNonEmptyString(avatarUrl);
|
||||
|
||||
const placeholderChar = placeholder?.[0]?.toLocaleUpperCase();
|
||||
|
||||
const showPlaceholder = noAvatarUrl || isInvalidAvatarUrl;
|
||||
|
||||
const handleImageError = () => {
|
||||
setIsInvalidAvatarUrl(true);
|
||||
};
|
||||
|
||||
const fixedColor = color ?? stringToHslColor(entityId ?? '', 75, 25);
|
||||
const fixedBackgroundColor =
|
||||
backgroundColor ??
|
||||
(!isNonEmptyString(avatarUrl)
|
||||
? stringToHslColor(entityId ?? '', 75, 85)
|
||||
: 'none');
|
||||
backgroundColor ?? stringToHslColor(entityId ?? '', 75, 85);
|
||||
|
||||
const showBackgroundColor = showPlaceholder;
|
||||
|
||||
return (
|
||||
<StyledAvatar
|
||||
className={className}
|
||||
avatarUrl={avatarUrl}
|
||||
placeholder={placeholder}
|
||||
size={size}
|
||||
type={type}
|
||||
entityId={entityId}
|
||||
<div
|
||||
className={clsx({
|
||||
[styles.avatar]: true,
|
||||
[styles.rounded]: type === 'rounded',
|
||||
[styles.avatarOnClick]: !isUndefined(onClick),
|
||||
})}
|
||||
onClick={onClick}
|
||||
color={fixedColor}
|
||||
backgroundColor={fixedBackgroundColor}
|
||||
style={{
|
||||
color: fixedColor,
|
||||
backgroundColor: showBackgroundColor ? fixedBackgroundColor : 'none',
|
||||
width: propertiesBySize[size].width,
|
||||
height: propertiesBySize[size].width,
|
||||
fontSize: propertiesBySize[size].fontSize,
|
||||
}}
|
||||
>
|
||||
{(noAvatarUrl || isInvalidAvatarUrl) &&
|
||||
placeholder?.[0]?.toLocaleUpperCase()}
|
||||
</StyledAvatar>
|
||||
{showPlaceholder ? (
|
||||
placeholderChar
|
||||
) : (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
className={styles.avatarImage}
|
||||
onError={handleImageError}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
.label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chip {
|
||||
--chip-horizontal-padding: calc(var(--twentycrm-spacing-multiplicator) * 1px);
|
||||
--chip-vertical-padding: calc(var(--twentycrm-spacing-multiplicator) * 1px);
|
||||
|
||||
align-items: center;
|
||||
border-radius: var(--twentycrm-border-radius-sm);
|
||||
|
||||
color: var(--twentycrm-font-color-secondary);
|
||||
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
|
||||
gap: calc(var(--twentycrm-spacing-multiplicator) * 1px);
|
||||
height: calc(var(--twentycrm-spacing-multiplicator) * 3px);
|
||||
|
||||
max-width: calc(100% - var(--chip-horizontal-padding) * 2px);
|
||||
overflow: hidden;
|
||||
|
||||
padding: var(--chip-vertical-padding) var(--chip-horizontal-padding);
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
color: var(--twentycrm-font-color-light);
|
||||
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.accent-text-primary {
|
||||
color: var(--twentycrm-font-color-primary);
|
||||
}
|
||||
|
||||
.accent-text-secondary {
|
||||
font-weight: var(--twentycrm-font-weight-medium);
|
||||
}
|
||||
|
||||
.size-large {
|
||||
height: calc(var(--twentycrm-spacing-multiplicator) * 4px);
|
||||
}
|
||||
|
||||
.variant-regular:hover {
|
||||
background-color: var(--twentycrm-background-transparent-light);
|
||||
}
|
||||
|
||||
.variant-regular:active {
|
||||
background-color: var(--twentycrm-background-transparent-medium);
|
||||
}
|
||||
|
||||
.variant-highlighted {
|
||||
background-color: var(--twentycrm-background-transparent-light);
|
||||
}
|
||||
|
||||
.variant-highlighted:hover {
|
||||
background-color: var(--twentycrm-background-transparent-medium);
|
||||
}
|
||||
|
||||
.variant-highlighted:active {
|
||||
background-color: var(--twentycrm-background-transparent-strong);
|
||||
}
|
||||
|
||||
.variant-rounded {
|
||||
--chip-horizontal-padding: calc(var(--twentycrm-spacing-multiplicator) * 2px);
|
||||
--chip-vertical-padding: 3px;
|
||||
|
||||
background-color: var(--twentycrm-background-transparent-light);
|
||||
border: 1px solid var(--twentycrm-border-color-medium);
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.variant-transparent {
|
||||
cursor: inherit;
|
||||
}
|
||||
@ -1,11 +1,10 @@
|
||||
import { MouseEvent, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import isPropValid from '@emotion/is-prop-valid';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
|
||||
|
||||
import styles from './Chip.module.css';
|
||||
|
||||
export enum ChipSize {
|
||||
Large = 'large',
|
||||
Small = 'small',
|
||||
@ -38,121 +37,6 @@ type ChipProps = {
|
||||
to?: string;
|
||||
};
|
||||
|
||||
const StyledContainer = styled('div', {
|
||||
shouldForwardProp: (prop) =>
|
||||
!['clickable', 'maxWidth'].includes(prop) && isPropValid(prop),
|
||||
})<
|
||||
Pick<
|
||||
ChipProps,
|
||||
'accent' | 'clickable' | 'disabled' | 'maxWidth' | 'size' | 'variant' | 'to'
|
||||
>
|
||||
>`
|
||||
--chip-horizontal-padding: ${({ theme }) => theme.spacing(1)};
|
||||
--chip-vertical-padding: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
text-decoration: none;
|
||||
align-items: center;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme, disabled }) =>
|
||||
disabled ? theme.font.color.light : theme.font.color.secondary};
|
||||
cursor: ${({ clickable, disabled }) =>
|
||||
clickable ? 'pointer' : disabled ? 'not-allowed' : 'inherit'};
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(3)};
|
||||
max-width: ${({ maxWidth }) =>
|
||||
maxWidth
|
||||
? `calc(${maxWidth}px - 2 * var(--chip-horizontal-padding))`
|
||||
: '200px'};
|
||||
overflow: hidden;
|
||||
padding: var(--chip-vertical-padding) var(--chip-horizontal-padding);
|
||||
user-select: none;
|
||||
|
||||
// Accent style overrides
|
||||
${({ accent, disabled, theme }) => {
|
||||
if (accent === ChipAccent.TextPrimary) {
|
||||
return (
|
||||
!disabled &&
|
||||
css`
|
||||
color: ${theme.font.color.primary};
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
if (accent === ChipAccent.TextSecondary) {
|
||||
return css`
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
`;
|
||||
}
|
||||
}}
|
||||
|
||||
// Variant style overrides
|
||||
${({ disabled, theme, variant }) => {
|
||||
if (variant === ChipVariant.Regular) {
|
||||
return (
|
||||
!disabled &&
|
||||
css`
|
||||
:hover {
|
||||
background-color: ${theme.background.transparent.light};
|
||||
}
|
||||
|
||||
:active {
|
||||
background-color: ${theme.background.transparent.medium};
|
||||
}
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === ChipVariant.Highlighted) {
|
||||
return css`
|
||||
background-color: ${theme.background.transparent.light};
|
||||
|
||||
${!disabled &&
|
||||
css`
|
||||
:hover {
|
||||
background-color: ${theme.background.transparent.medium};
|
||||
}
|
||||
|
||||
:active {
|
||||
background-color: ${theme.background.transparent.strong};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
if (variant === ChipVariant.Rounded) {
|
||||
return css`
|
||||
--chip-horizontal-padding: ${theme.spacing(2)};
|
||||
--chip-vertical-padding: 3px;
|
||||
|
||||
background-color: ${theme.background.transparent.lighter};
|
||||
border: 1px solid ${theme.border.color.medium};
|
||||
border-radius: 50px;
|
||||
`;
|
||||
}
|
||||
|
||||
if (variant === ChipVariant.Transparent) {
|
||||
return css`
|
||||
cursor: inherit;
|
||||
`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledOverflowingTextWithTooltip = styled(OverflowingTextWithTooltip)<{
|
||||
size?: ChipSize;
|
||||
}>`
|
||||
height: ${({ theme, size }) =>
|
||||
size === ChipSize.Large ? theme.spacing(4) : 'auto'};
|
||||
`;
|
||||
|
||||
export const Chip = ({
|
||||
size = ChipSize.Small,
|
||||
label,
|
||||
@ -162,30 +46,33 @@ export const Chip = ({
|
||||
leftComponent,
|
||||
rightComponent,
|
||||
accent = ChipAccent.TextPrimary,
|
||||
maxWidth,
|
||||
className,
|
||||
onClick,
|
||||
to,
|
||||
}: ChipProps) => {
|
||||
return (
|
||||
<StyledContainer
|
||||
<div
|
||||
data-testid="chip"
|
||||
clickable={clickable}
|
||||
variant={variant}
|
||||
accent={accent}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
className={clsx({
|
||||
[styles.chip]: true,
|
||||
[styles.clickable]: clickable,
|
||||
[styles.disabled]: disabled,
|
||||
[styles.accentTextPrimary]: accent === ChipAccent.TextPrimary,
|
||||
[styles.accentTextSecondary]: accent === ChipAccent.TextSecondary,
|
||||
[styles.sizeLarge]: size === ChipSize.Large,
|
||||
[styles.variantRegular]: variant === ChipVariant.Regular,
|
||||
[styles.variantHighlighted]: variant === ChipVariant.Highlighted,
|
||||
[styles.variantRounded]: variant === ChipVariant.Rounded,
|
||||
[styles.variantTransparent]: variant === ChipVariant.Transparent,
|
||||
})}
|
||||
onClick={onClick}
|
||||
as={to ? Link : 'div'}
|
||||
to={to ? to : undefined}
|
||||
>
|
||||
{leftComponent}
|
||||
<StyledLabel>
|
||||
<StyledOverflowingTextWithTooltip size={size} text={label} />
|
||||
</StyledLabel>
|
||||
<div className={styles.label}>
|
||||
<OverflowingTextWithTooltip
|
||||
size={size === ChipSize.Large ? 'large' : 'small'}
|
||||
text={label}
|
||||
/>
|
||||
</div>
|
||||
{rightComponent}
|
||||
</StyledContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,11 +3,10 @@ import { useTheme } from '@emotion/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { Avatar, AvatarType } from '@ui/display/avatar/components/Avatar';
|
||||
import { Chip, ChipVariant } from '@ui/display/chip/components/Chip';
|
||||
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
||||
import { Nullable } from '@ui/utilities/types/Nullable';
|
||||
|
||||
import { Chip, ChipVariant } from './Chip';
|
||||
|
||||
export type EntityChipProps = {
|
||||
linkToEntity?: string;
|
||||
entityId: string;
|
||||
@ -17,7 +16,6 @@ export type EntityChipProps = {
|
||||
variant?: EntityChipVariant;
|
||||
LeftIcon?: IconComponent;
|
||||
className?: string;
|
||||
maxWidth?: number;
|
||||
};
|
||||
|
||||
export enum EntityChipVariant {
|
||||
@ -34,7 +32,6 @@ export const EntityChip = ({
|
||||
variant = EntityChipVariant.Regular,
|
||||
LeftIcon,
|
||||
className,
|
||||
maxWidth,
|
||||
}: EntityChipProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@ -70,8 +67,6 @@ export const EntityChip = ({
|
||||
clickable={!!linkToEntity}
|
||||
onClick={handleLinkClick}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
to={linkToEntity}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
.main {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
|
||||
font-weight: inherit;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-decoration: inherit;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.large {
|
||||
height: calc(var(--twentycrm-spacing-multiplicator) * 4px);
|
||||
}
|
||||
@ -1,41 +1,40 @@
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import clsx from 'clsx';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import { AppTooltip } from './AppTooltip';
|
||||
|
||||
const StyledOverflowingText = styled.div<{ cursorPointer: boolean }>`
|
||||
cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'inherit')};
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
|
||||
font-weight: inherit;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-decoration: inherit;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
import styles from './OverflowingTextWithTooltip.module.css';
|
||||
|
||||
export const OverflowingTextWithTooltip = ({
|
||||
size = 'small',
|
||||
text,
|
||||
className,
|
||||
mutliline,
|
||||
}: {
|
||||
size?: 'large' | 'small';
|
||||
text: string | null | undefined;
|
||||
className?: string;
|
||||
mutliline?: boolean;
|
||||
}) => {
|
||||
const textElementId = `title-id-${uuidV4()}`;
|
||||
const [textElement, setTextElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const isTitleOverflowing =
|
||||
(text?.length ?? 0) > 0 &&
|
||||
!!textElement &&
|
||||
(textElement.scrollHeight > textElement.clientHeight ||
|
||||
textElement.scrollWidth > textElement.clientWidth);
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isTitleOverflowing, setIsTitleOverflowing] = useState(false);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
const isOverflowing =
|
||||
(text?.length ?? 0) > 0 && textRef.current
|
||||
? textRef.current?.scrollHeight > textRef.current?.clientHeight ||
|
||||
textRef.current.scrollWidth > textRef.current.clientWidth
|
||||
: false;
|
||||
|
||||
setIsTitleOverflowing(isOverflowing);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsTitleOverflowing(false);
|
||||
};
|
||||
|
||||
const handleTooltipClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
@ -44,23 +43,29 @@ export const OverflowingTextWithTooltip = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledOverflowingText
|
||||
<div
|
||||
data-testid="tooltip"
|
||||
className={className}
|
||||
ref={setTextElement}
|
||||
className={clsx({
|
||||
[styles.main]: true,
|
||||
[styles.cursor]: isTitleOverflowing,
|
||||
[styles.large]: size === 'large',
|
||||
})}
|
||||
ref={textRef}
|
||||
id={textElementId}
|
||||
cursorPointer={isTitleOverflowing}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{text}
|
||||
</StyledOverflowingText>
|
||||
</div>
|
||||
{isTitleOverflowing &&
|
||||
createPortal(
|
||||
<div onClick={handleTooltipClick}>
|
||||
<AppTooltip
|
||||
anchorSelect={`#${textElementId}`}
|
||||
content={mutliline ? undefined : text ?? ''}
|
||||
delayHide={0}
|
||||
delayHide={1}
|
||||
offset={5}
|
||||
isOpen
|
||||
noArrow
|
||||
place="bottom"
|
||||
positionStrategy="absolute"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export const BORDER_COMMON = {
|
||||
radius: {
|
||||
xs: '2px',
|
||||
sm: '4px',
|
||||
sm: 'var(--twentycrm-border-radius-sm)',
|
||||
md: '8px',
|
||||
xl: '20px',
|
||||
pill: '999px',
|
||||
|
||||
@ -10,7 +10,7 @@ export const FONT_COMMON = {
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
medium: 'var(--twentycrm-border-radius-sm)',
|
||||
semiBold: 600,
|
||||
},
|
||||
family: 'Inter, sans-serif',
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
// ThemeProvider.tsx
|
||||
import * as React from 'react';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { ThemeProvider as EmotionThemeProvider } from '@emotion/react';
|
||||
|
||||
import { ThemeType } from '..';
|
||||
|
||||
import './theme.css';
|
||||
|
||||
type ThemeProviderProps = {
|
||||
theme: ThemeType;
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const ThemeProvider: React.FC<ThemeProviderProps> = ({ theme, children }) => {
|
||||
const ThemeProvider = ({ theme, children }: ThemeProviderProps) => {
|
||||
useEffect(() => {
|
||||
document.documentElement.className =
|
||||
theme.name === 'dark' ? 'dark' : 'light';
|
||||
}, [theme]);
|
||||
|
||||
return <EmotionThemeProvider theme={theme}>{children}</EmotionThemeProvider>;
|
||||
};
|
||||
|
||||
|
||||
85
packages/twenty-ui/src/theme/provider/theme.css
Normal file
85
packages/twenty-ui/src/theme/provider/theme.css
Normal file
@ -0,0 +1,85 @@
|
||||
:root {
|
||||
--twentycrm-spacing-multiplicator: 4;
|
||||
--twentycrm-border-radius-sm: 4px;
|
||||
--twentycrm-font-weight-medium: 500;
|
||||
|
||||
/* Grays */
|
||||
--twentycrm-gray-100: #000000;
|
||||
--twentycrm-gray-100-4: #0000000A;
|
||||
--twentycrm-gray-100-10: #00000019;
|
||||
--twentycrm-gray-100-16: #00000029;
|
||||
--twentycrm-gray-90: #141414;
|
||||
--twentycrm-gray-85: #171717;
|
||||
--twentycrm-gray-85-80: #171717CC;
|
||||
--twentycrm-gray-80: #1b1b1b;
|
||||
--twentycrm-gray-80-80: #1b1b1bCC;
|
||||
--twentycrm-gray-75: #1d1d1d;
|
||||
--twentycrm-gray-70: #222222;
|
||||
--twentycrm-gray-65: #292929;
|
||||
--twentycrm-gray-60: #333333;
|
||||
--twentycrm-gray-55: #4c4c4c;
|
||||
--twentycrm-gray-50: #666666;
|
||||
--twentycrm-gray-45: #818181;
|
||||
--twentycrm-gray-40: #999999;
|
||||
--twentycrm-gray-35: #b3b3b3;
|
||||
--twentycrm-gray-30: #cccccc;
|
||||
--twentycrm-gray-25: #d6d6d6;
|
||||
--twentycrm-gray-20: #ebebeb;
|
||||
--twentycrm-gray-15: #f1f1f1;
|
||||
--twentycrm-gray-10: #fcfcfc;
|
||||
--twentycrm-gray-10-80: #fcfcfcCC;
|
||||
--twentycrm-gray-0: #ffffff;
|
||||
--twentycrm-gray-0-6: #ffffff0f;
|
||||
--twentycrm-gray-0-10: #ffffff19;
|
||||
--twentycrm-gray-0-14: #ffffff23;
|
||||
|
||||
/* Blues */
|
||||
--twentycrm-blue-accent-90: #141a25,
|
||||
--twentycrm-blue-accent-10: #f5f9fd,
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
/* Accent color */
|
||||
--twentycrm-accent-quaternary: var(--twentycrm-blue-accent-90);
|
||||
|
||||
/* Font color */
|
||||
--twentycrm-font-color-secondary: var(--twentycrm-gray-35);
|
||||
--twentycrm-font-color-primary: var(--twentycrm-gray-20);
|
||||
--twentycrm-font-color-light: var(--twentycrm-gray-50);
|
||||
--twentycrm-font-color-extra-light: var(--twentycrm-gray-55);
|
||||
|
||||
/* Background color */
|
||||
--twentycrm-background-primary: var(--twentycrm-gray-85);
|
||||
|
||||
/* Background transparent color */
|
||||
--twentycrm-background-transparent-secondary: var(--twentycrm-gray-80-80);
|
||||
--twentycrm-background-transparent-light: var(--twentycrm-gray-0-6);
|
||||
--twentycrm-background-transparent-medium: var(--twentycrm-gray-0-10);
|
||||
--twentycrm-background-transparent-strong: var(--twentycrm-gray-0-14);
|
||||
|
||||
/* Border color */
|
||||
--twentycrm-border-color-medium: var(--twentycrm-gray-65);
|
||||
}
|
||||
|
||||
:root.light {
|
||||
/* Accent color */
|
||||
--twentycrm-accent-quaternary: var(--twentycrm-blue-accent-10);
|
||||
|
||||
/* Colors */
|
||||
--twentycrm-font-color-primary: var(--twentycrm-gray-60);
|
||||
--twentycrm-font-color-secondary: var(--twentycrm-gray-50);
|
||||
--twentycrm-font-color-light: var(--twentycrm-gray-35);
|
||||
--twentycrm-font-color-extra-light: var(--twentycrm-gray-30);
|
||||
|
||||
/* Background color */
|
||||
--twentycrm-background-primary: var(--twentycrm-gray-0);
|
||||
|
||||
/* Background transparent color */
|
||||
--twentycrm-background-transparent-secondary: var(--twentycrm-gray-10-80);
|
||||
--twentycrm-background-transparent-light: var(--twentycrm-gray-100-4);
|
||||
--twentycrm-background-transparent-medium: var(--twentycrm-gray-100-10);
|
||||
--twentycrm-background-transparent-strong: var(--twentycrm-gray-100-16);
|
||||
|
||||
/* Border color */
|
||||
--twentycrm-border-color-medium: var(--twentycrm-gray-20);
|
||||
}
|
||||
Reference in New Issue
Block a user